]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
dokan: simple case insensitive emulation
authorLucian Petrut <lpetrut@cloudbasesolutions.com>
Wed, 13 Sep 2023 12:12:26 +0000 (12:12 +0000)
committerLucian Petrut <lpetrut@cloudbasesolutions.com>
Thu, 14 Sep 2023 08:49:47 +0000 (08:49 +0000)
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 <lpetrut@cloudbasesolutions.com>
src/dokan/ceph_dokan.cc
src/dokan/ceph_dokan.h
src/dokan/options.cc
src/test/dokan/dokan.cc

index 9e115222cab2f3def2ecba578521efd485a78c67..6459ea261bf1de187261c123a243a1b0c78b72da 100644 (file)
@@ -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;
index 5957d4dead11e7aae8901985b11c82267504e0d8..fe48aa458143b390141f3aca2d69fa2e60b3cdca 100644 (file)
@@ -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;
index 1ed90ef9d34dfaa763c28217b6bdc55f80a8e603..705e1117ca9928e48df2d486bb97aeaf15677428 100644 (file)
@@ -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;
     }
index 18f206985e8b186f44faf30b0b56a87be24d5885..eaa26557fe88429e5e83e9d1525d8d8309f48015 100644 (file)
@@ -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<std::string> 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);
+}