diff --git a/src/node_file.cc b/src/node_file.cc index d93f213202ec43..2811556fff8640 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -1745,6 +1745,45 @@ static void RMDir(const FunctionCallbackInfo& args) { } } +#ifdef _WIN32 +static void ClearReadOnlyAttributeW( + const std::filesystem::path& path, + bool recursive) { + std::error_code ec; + if (recursive) { + auto file_status = std::filesystem::symlink_status(path, ec); + if (!ec && file_status.type() == std::filesystem::file_type::directory) { + for (const auto& entry : std::filesystem::recursive_directory_iterator( + path, + std::filesystem::directory_options::skip_permission_denied, + ec)) { + std::error_code entry_ec; + auto entry_status = entry.symlink_status(entry_ec); + if (entry_ec) continue; + if (entry_status.type() != std::filesystem::file_type::symlink) { + DWORD attrs = GetFileAttributesW(entry.path().c_str()); + if (attrs != INVALID_FILE_ATTRIBUTES && + (attrs & FILE_ATTRIBUTE_READONLY)) { + SetFileAttributesW( + entry.path().c_str(), + attrs & ~FILE_ATTRIBUTE_READONLY); + } + } + } + } + } + + auto file_status = std::filesystem::symlink_status(path, ec); + if (!ec && file_status.type() != std::filesystem::file_type::symlink) { + DWORD attrs = GetFileAttributesW(path.c_str()); + if (attrs != INVALID_FILE_ATTRIBUTES && + (attrs & FILE_ATTRIBUTE_READONLY)) { + SetFileAttributesW(path.c_str(), attrs & ~FILE_ATTRIBUTE_READONLY); + } + } +} +#endif + static void RmSync(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = env->isolate(); @@ -1799,7 +1838,27 @@ static void RmSync(const FunctionCallbackInfo& args) { if (!error || error == std::errc::no_such_file_or_directory) { return; - } else if (!can_omit_error(error)) { + } + +#ifdef _WIN32 + // On Windows, libc++ does not clear the read-only attribute before + // removing a file (unlike MSVC STL which does). Attempt to clear it + // manually when we get EPERM (operation_not_permitted) so that read-only + // files can be deleted, matching the behavior of official Node.js builds. + if (error == std::errc::operation_not_permitted) { + ClearReadOnlyAttributeW(file_path, recursive); + if (recursive) { + std::filesystem::remove_all(file_path, error); + } else { + std::filesystem::remove(file_path, error); + } + if (!error || error == std::errc::no_such_file_or_directory) { + return; + } + } +#endif // _WIN32 + + if (!can_omit_error(error)) { break; } diff --git a/test/parallel/test-fs-rm.js b/test/parallel/test-fs-rm.js index e92bf8c079715f..1d0578e3004da4 100644 --- a/test/parallel/test-fs-rm.js +++ b/test/parallel/test-fs-rm.js @@ -631,3 +631,30 @@ if (isGitPresent) { } } } + +{ + // Test that rmSync can delete read-only files (and directories containing read-only files recursively) + const dirname = nextDirPath(); + const filePath = path.join(dirname, 'readonly-file.txt'); + const recursiveDir = path.join(dirname, 'subdir'); + const recursiveFilePath = path.join(recursiveDir, 'readonly-nested.txt'); + + fs.mkdirSync(recursiveDir, { recursive: true }); + fs.writeFileSync(filePath, 'hello'); + fs.writeFileSync(recursiveFilePath, 'world'); + + // Make files read-only + fs.chmodSync(filePath, 0o444); + fs.chmodSync(recursiveFilePath, 0o444); + + // rmSync without recursive option on a file + fs.rmSync(filePath); + assert.strictEqual(fs.existsSync(filePath), false); + + // rmSync with recursive option on a directory containing a read-only file + fs.rmSync(recursiveDir, { recursive: true }); + assert.strictEqual(fs.existsSync(recursiveDir), false); + + // Clean up parent directory + fs.rmSync(dirname, { recursive: true, force: true }); +}