Prechádzať zdrojové kódy

[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>
Sergey Gavrilov 2 rokov pred
rodič
commit
777a4d109d
52 zmenil súbory, kde vykonal 870 pridanie a 576 odobranie
  1. 3 0
      .pvsconfig
  2. 4 5
      applications/debug/unit_tests/rpc/rpc_test.c
  3. 4 4
      applications/debug/unit_tests/storage/dirwalk_test.c
  4. 291 1
      applications/debug/unit_tests/storage/storage_test.c
  5. 18 0
      applications/examples/example_apps_data/README.md
  6. 9 0
      applications/examples/example_apps_data/application.fam
  7. 40 0
      applications/examples/example_apps_data/example_apps_data.c
  8. 1 1
      applications/main/archive/helpers/archive_browser.c
  9. 1 1
      applications/main/archive/helpers/archive_favorites.c
  10. 1 1
      applications/main/archive/helpers/archive_files.c
  11. 7 0
      applications/main/fap_loader/fap_loader_app.c
  12. 11 1
      applications/plugins/hid_app/hid.c
  13. 1 1
      applications/plugins/hid_app/hid.h
  14. 8 3
      applications/plugins/music_player/music_player.c
  15. 10 10
      applications/plugins/picopass/helpers/iclass_elite_dict.c
  16. 8 0
      applications/plugins/picopass/picopass.c
  17. 5 9
      applications/plugins/picopass/picopass_device.c
  18. 0 2
      applications/plugins/picopass/picopass_device.h
  19. 1 3
      applications/plugins/picopass/scenes/picopass_scene_save_name.c
  20. 1 1
      applications/plugins/spi_mem_manager/scenes/spi_mem_scene_start.c
  21. 5 5
      applications/plugins/spi_mem_manager/spi_mem_app.c
  22. 0 1
      applications/plugins/spi_mem_manager/spi_mem_app_i.h
  23. 1 7
      applications/plugins/spi_mem_manager/spi_mem_files.c
  24. 0 1
      applications/plugins/spi_mem_manager/spi_mem_files.h
  25. 1 0
      applications/services/applications.h
  26. 8 1
      applications/services/bt/bt_service/bt_api.c
  27. 9 2
      applications/services/cli/cli_commands.c
  28. 21 1
      applications/services/dialogs/dialogs_api.c
  29. 6 6
      applications/services/gui/modules/file_browser_worker.c
  30. 2 0
      applications/services/loader/loader.c
  31. 4 5
      applications/services/rpc/rpc_storage.c
  32. 4 0
      applications/services/storage/filesystem_api.c
  33. 9 2
      applications/services/storage/filesystem_api_defines.h
  34. 41 0
      applications/services/storage/storage.h
  35. 3 3
      applications/services/storage/storage_cli.c
  36. 81 22
      applications/services/storage/storage_external_api.c
  37. 1 10
      applications/services/storage/storage_glue.c
  38. 1 6
      applications/services/storage/storage_glue.h
  39. 2 0
      applications/services/storage/storage_i.h
  40. 13 0
      applications/services/storage/storage_message.h
  41. 145 115
      applications/services/storage/storage_processing.c
  42. 0 341
      applications/services/storage/storage_test_app.c
  43. 1 1
      applications/system/storage_move_to_sd/storage_move_to_sd.c
  44. 0 0
      assets/resources/apps_data/music_player/Marble_Machine.fmf
  45. 0 0
      assets/resources/apps_data/picopass/assets/iclass_elite_dict.txt
  46. 8 1
      firmware/targets/f7/api_symbols.csv
  47. 39 1
      furi/core/thread.c
  48. 37 0
      furi/core/thread.h
  49. 1 0
      furi/flipper.c
  50. 1 1
      lib/toolbox/dir_walk.c
  51. 1 1
      lib/toolbox/tar/tar_archive.c
  52. 1 0
      scripts/fbt/appmanifest.py

+ 3 - 0
.pvsconfig

@@ -44,3 +44,6 @@
 
 # Functions that always return the same error code
 //-V:picopass_device_decrypt:1048
+
+# Examples
+//V_EXCLUDE_PATH applications/examples/

+ 4 - 5
applications/debug/unit_tests/rpc/rpc_test.c

@@ -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;

+ 4 - 4
applications/debug/unit_tests/storage/dirwalk_test.c

@@ -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);

+ 291 - 1
applications/debug/unit_tests/storage/storage_test.c

@@ -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 - 0
applications/examples/example_apps_data/README.md

@@ -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 - 0
applications/examples/example_apps_data/application.fam

@@ -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 - 0
applications/examples/example_apps_data/example_apps_data.c

@@ -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;
+}

+ 1 - 1
applications/main/archive/helpers/archive_browser.c

@@ -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;
         }
     }

+ 1 - 1
applications/main/archive/helpers/archive_favorites.c

@@ -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;

+ 1 - 1
applications/main/archive/helpers/archive_files.c

@@ -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);

+ 7 - 0
applications/main/fap_loader/fap_loader_app.c

@@ -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);
 

+ 11 - 1
applications/plugins/hid_app/hid.c

@@ -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");

+ 1 - 1
applications/plugins/hid_app/hid.h

@@ -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,

+ 8 - 3
applications/plugins/music_player/music_player.c

@@ -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;

+ 10 - 10
applications/plugins/picopass/helpers/iclass_elite_dict.c

@@ -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;

+ 8 - 0
applications/plugins/picopass/picopass.c

@@ -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);

+ 5 - 9
applications/plugins/picopass/picopass_device.c

@@ -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;

+ 0 - 2
applications/plugins/picopass/picopass_device.h

@@ -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();

+ 1 - 3
applications/plugins/picopass/scenes/picopass_scene_save_name.c

@@ -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(

+ 1 - 1
applications/plugins/spi_mem_manager/scenes/spi_mem_scene_start.c

@@ -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) {

+ 5 - 5
applications/plugins/spi_mem_manager/spi_mem_app.c

@@ -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;

+ 0 - 1
applications/plugins/spi_mem_manager/spi_mem_app_i.h

@@ -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 - 7
applications/plugins/spi_mem_manager/spi_mem_files.c

@@ -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;

+ 0 - 1
applications/plugins/spi_mem_manager/spi_mem_files.h

@@ -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);

+ 1 - 0
applications/services/applications.h

@@ -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;

+ 8 - 1
applications/services/bt/bt_service/bt_api.c

@@ -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) {

+ 9 - 2
applications/services/cli/cli_commands.c

@@ -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]),

+ 21 - 1
applications/services/dialogs/dialogs_api.c

@@ -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;
 }
 

+ 6 - 6
applications/services/gui/modules/file_browser_worker.c

@@ -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++;
                 }

+ 2 - 0
applications/services/loader/loader.c

@@ -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(

+ 4 - 5
applications/services/rpc/rpc_storage.c

@@ -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);

+ 4 - 0
applications/services/storage/filesystem_api.c

@@ -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);
+}

+ 9 - 2
applications/services/storage/filesystem_api_defines.h

@@ -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

+ 41 - 0
applications/services/storage/storage.h

@@ -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

+ 3 - 3
applications/services/storage/storage_cli.c

@@ -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));

+ 81 - 22
applications/services/storage/storage_external_api.c

@@ -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;

+ 1 - 10
applications/services/storage/storage_glue.c

@@ -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);
 }
 

+ 1 - 6
applications/services/storage/storage_glue.h

@@ -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

+ 2 - 0
applications/services/storage/storage_i.h

@@ -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;

+ 13 - 0
applications/services/storage/storage_message.h

@@ -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 {

+ 145 - 115
applications/services/storage/storage_processing.c

@@ -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,27 +24,23 @@ 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) {
+    const char* path_cstr = furi_string_get_cstr(path);
+
+    if(memcmp(path_cstr, 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) {
+    } else if(memcmp(path_cstr, 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) {
+    } else if(memcmp(path_cstr, STORAGE_ANY_PATH_PREFIX, strlen(STORAGE_ANY_PATH_PREFIX)) == 0) {
         type = ST_ANY;
     }
 
-    if(type == ST_ANY) {
-        type = ST_INT;
-        if(storage_data_status(&app->storage[ST_EXT]) == StorageStatusOK) {
-            type = ST_EXT;
-        }
-    }
-
     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;
-
-    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);
+    file->error_id = storage_get_data(app, path, &storage);
 
-        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;
-
-    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);
+    file->error_id = storage_get_data(app, path, &storage);
 
-        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);
 }
 

+ 0 - 341
applications/services/storage/storage_test_app.c

@@ -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;
-}

+ 1 - 1
applications/system/storage_move_to_sd/storage_move_to_sd.c

@@ -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;
     }
 

+ 0 - 0
assets/resources/music_player/Marble_Machine.fmf → assets/resources/apps_data/music_player/Marble_Machine.fmf


+ 0 - 0
assets/resources/picopass/assets/iclass_elite_dict.txt → assets/resources/apps_data/picopass/assets/iclass_elite_dict.txt


+ 8 - 1
firmware/targets/f7/api_symbols.csv

@@ -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*

+ 39 - 1
furi/core/thread.c

@@ -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;

+ 37 - 0
furi/core/thread.h

@@ -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

+ 1 - 0
furi/flipper.c

@@ -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);
     }

+ 1 - 1
lib/toolbox/dir_walk.c

@@ -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;

+ 1 - 1
lib/toolbox/tar/tar_archive.c

@@ -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(

+ 1 - 0
scripts/fbt/appmanifest.py

@@ -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)} }}"""