From 7d8db4954c2fedb4db9f0ccc8a9d304f35807b07 Mon Sep 17 00:00:00 2001 From: Lucian Petrut Date: Wed, 13 Sep 2023 12:12:26 +0000 Subject: [PATCH] dokan: simple case insensitive emulation While CephFS is case sensitive, Windows software commonly assume that the filesystem is case insensitive. In order to unblock certain use cases, a short term solution is to simply normalize paths when creating or accessing files or directories. This change adds two ceph-dokan parameters: * --case-insensitive: if set, paths are normalized * --force-lowercase: normalized paths are converted to lowercase instead of uppercase This trivial implementation has some limitations: * the original case is not preserved * we could later on use xattr to store the original name * can't access existing files that have a different case * handled at ceph-dokan level The advantage is that it's simple, shouldn't impact performance and doesn't require libcephfs or mds changes. In the future, we may conider a more robust implementation at the mds and/or libcephfs level. Signed-off-by: Lucian Petrut --- src/dokan/ceph_dokan.cc | 40 +++++++++++++++---- src/dokan/ceph_dokan.h | 8 ++++ src/dokan/options.cc | 9 +++++ src/test/dokan/dokan.cc | 86 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 7 deletions(-) diff --git a/src/dokan/ceph_dokan.cc b/src/dokan/ceph_dokan.cc index 9e115222cab2..6459ea261bf1 100644 --- a/src/dokan/ceph_dokan.cc +++ b/src/dokan/ceph_dokan.cc @@ -77,9 +77,26 @@ typedef struct { static_assert(sizeof(fd_context) <= 8, "fd_context exceeds DOKAN_FILE_INFO.Context size."); -string get_path(LPCWSTR path_w) { +string get_path(LPCWSTR path_w, bool normalize_case=true) { string path = to_string(path_w); replace(path.begin(), path.end(), '\\', '/'); + + if (normalize_case && !g_cfg->case_sensitive) { + if (g_cfg->convert_to_uppercase) { + std::transform( + path.begin(), path.end(), path.begin(), + [](unsigned char c){ + return std::toupper(c); + }); + } else { + std::transform( + path.begin(), path.end(), path.begin(), + [](unsigned char c){ + return std::tolower(c); + }); + } + } + return path; } @@ -543,6 +560,11 @@ static NTSTATUS WinCephFindFiles( return cephfs_errno_to_ntstatus_map(ret); } + // TODO: retrieve the original case (e.g. using xattr) if configured + // to do so. + // TODO: provide aliases when case insensitive mounts cause collisions. + // For example, when having test.txt and Test.txt, the latter becomes + // TEST~1.txt WIN32_FIND_DATAW findData; int count = 0; while (1) { @@ -794,14 +816,18 @@ static NTSTATUS WinCephGetVolumeInformation( { g_cfg->win_vol_name.copy(VolumeNameBuffer, VolumeNameSize); *VolumeSerialNumber = g_cfg->win_vol_serial; - *MaximumComponentLength = g_cfg->max_path_len; - *FileSystemFlags = FILE_CASE_SENSITIVE_SEARCH | - FILE_CASE_PRESERVED_NAMES | - FILE_SUPPORTS_REMOTE_STORAGE | - FILE_UNICODE_ON_DISK | - FILE_PERSISTENT_ACLS; + *FileSystemFlags = + FILE_SUPPORTS_REMOTE_STORAGE | + FILE_UNICODE_ON_DISK | + FILE_PERSISTENT_ACLS; + + if (g_cfg->case_sensitive) { + *FileSystemFlags |= + FILE_CASE_SENSITIVE_SEARCH | + FILE_CASE_PRESERVED_NAMES; + } wcscpy(FileSystemNameBuffer, L"Ceph"); return 0; diff --git a/src/dokan/ceph_dokan.h b/src/dokan/ceph_dokan.h index 5957d4dead11..fe48aa458143 100644 --- a/src/dokan/ceph_dokan.h +++ b/src/dokan/ceph_dokan.h @@ -36,6 +36,14 @@ struct Config { unsigned long max_path_len = 256; mode_t file_mode = 0755; mode_t dir_mode = 0755; + + bool case_sensitive = true; + // Convert new file paths to upper case in case of case insensitive mounts. + // Visual Studio recommends normalizing to uppercase in order to avoid + // locale issues (CA1308). + bool convert_to_uppercase = true; + // TODO: consider adding an option to preserve the original case. + // It could be stored using an extended attribute. }; extern Config *g_cfg; diff --git a/src/dokan/options.cc b/src/dokan/options.cc index 1ed90ef9d34d..705e1117ca99 100644 --- a/src/dokan/options.cc +++ b/src/dokan/options.cc @@ -45,6 +45,11 @@ Map options: --max-path-len The value of the maximum path length. Default: 256. --file-mode The access mode to be used when creating files. --dir-mode The access mode to be used when creating directories. + --case-insensitive Emulate a case insensitive filesystem by normalizing + paths. The original case is NOT preserved. Existing + paths with a different case cannot be accessed. + --force-lowercase Use lowercase when normalizing paths. Uppercase is + used by default. Unmap options: -l [ --mountpoint ] arg mountpoint (path or drive letter) (e.g -l x). @@ -196,6 +201,10 @@ int parse_args( *err_msg << "ceph-dokan: Invalid argument for operation-timeout"; return -EINVAL; } + } else if (ceph_argparse_flag(args, i, "--case-insensitive", (char *)NULL)) { + cfg->case_sensitive = false; + } else if (ceph_argparse_flag(args, i, "--force-lowercase", (char *)NULL)) { + cfg->convert_to_uppercase = false; } else { ++i; } diff --git a/src/test/dokan/dokan.cc b/src/test/dokan/dokan.cc index 18f206985e8b..eaa26557fe88 100644 --- a/src/test/dokan/dokan.cc +++ b/src/test/dokan/dokan.cc @@ -39,6 +39,18 @@ std::string get_uuid() { return suffix.to_string(); } +std::string to_upper(std::string& in) { + std::string out = in; + + std::transform( + out.begin(), out.end(), out.begin(), + [](unsigned char c){ + return std::toupper(c); + }); + + return out; +} + bool move_eof(HANDLE handle, LARGE_INTEGER offset) { // Move file pointer to FILE_BEGIN + offset @@ -162,6 +174,22 @@ void map_dokan_with_maxpath( } } +void map_dokan_case_insensitive(SubProcess** mount, const char* mountpoint, + bool force_lowercase=false) { + SubProcess* new_mount = new SubProcess("ceph-dokan"); + + new_mount->add_cmd_args("map", "--win-vol-name", "TestCeph", + "--win-vol-serial", TEST_VOL_SERIAL, + "-l", mountpoint, "--case-insensitive", NULL); + if (force_lowercase) { + new_mount->add_cmd_args("--force-lowercase", NULL); + } + + *mount = new_mount; + ASSERT_EQ(new_mount->spawn(), 0); + ASSERT_EQ(wait_for_mount(mountpoint), 0); +} + void unmap_dokan(SubProcess* mount, const char* mountpoint) { std::string ret = run_cmd("ceph-dokan", "unmap", "-l", mountpoint, (char*)NULL); @@ -762,3 +790,61 @@ TEST_F(DokanTests, test_create_dispositions) { // clean-up ASSERT_TRUE(fs::remove(file_path)); } + +TEST_F(DokanTests, test_case_sensitive) { + std::string test_dir = DEFAULT_MOUNTPOINT"test_dir" + get_uuid() + "\\"; + std::string lower_file_path = test_dir + "file_" + get_uuid(); + std::string upper_file_path = to_upper(lower_file_path); + + ASSERT_TRUE(fs::create_directory(test_dir)); + std::ofstream{lower_file_path}; + + ASSERT_TRUE(fs::exists(lower_file_path)); + ASSERT_FALSE(fs::exists(upper_file_path)); + + // clean-up + fs::remove_all(test_dir); +} + +void test_case_insensitive(bool force_lowercase) { + std::string mountpoint = "Q:\\"; + std::string test_dir = mountpoint + "test_dir" + get_uuid() + "/"; + std::string file_name = "file_" + get_uuid(); + std::string lower_file_path = test_dir + file_name; + std::string upper_file_path = to_upper(lower_file_path); + + SubProcess* mount = nullptr; + map_dokan_case_insensitive(&mount, mountpoint.c_str(), force_lowercase); + + ASSERT_TRUE(fs::create_directory(test_dir)); + std::ofstream{upper_file_path}; + + ASSERT_TRUE(fs::exists(lower_file_path)); + ASSERT_TRUE(fs::exists(upper_file_path)); + + std::vector paths; + for (const auto & entry : fs::recursive_directory_iterator(test_dir)) { + paths.push_back(entry.path().filename().generic_string()); + } + + bool found_lowercase = std::find( + begin(paths), end(paths), file_name) != end(paths); + bool found_uppercase = std::find( + begin(paths), end(paths), to_upper(file_name)) != end(paths); + + ASSERT_EQ(found_lowercase, force_lowercase); + ASSERT_NE(found_uppercase, force_lowercase); + + // clean-up + fs::remove_all(test_dir); + + unmap_dokan(mount, mountpoint.c_str()); +} + +TEST_F(DokanTests, test_case_insensitive_force_lower) { + test_case_insensitive(true); +} + +TEST_F(DokanTests, test_case_insensitive_force_upper) { + test_case_insensitive(false); +} -- 2.47.3