{"record":{"id":"620f3e2fbae1b231","repo":"puppetlabs/puppet","slug":"is-a-directory-directory","errorCode":null,"errorMessage":"Is a directory: %{directory}","messagePattern":"Is a directory: %(.+?)","errorType":"exception","errorClass":"Errno::EISDIR","httpStatus":null,"severity":"error","filePath":"lib/puppet/file_system/jruby.rb","lineNumber":20,"sourceCode":"\nrequire_relative '../../puppet/file_system/posix'\n\nclass Puppet::FileSystem::JRuby < Puppet::FileSystem::Posix\n  def unlink(*paths)\n    File.unlink(*paths)\n  rescue Errno::ENOENT\n    # JRuby raises ENOENT if the path doesn't exist or the parent directory\n    # doesn't allow execute/traverse. If it's the former, `stat` will raise\n    # ENOENT, if it's the later, it'll raise EACCES\n    # See https://github.com/jruby/jruby/issues/5617\n    stat(*paths)\n  end\n\n  def replace_file(path, mode = nil, &block)\n    # MRI Ruby rename checks if destination is a directory and raises, while\n    # JRuby removes the directory and replaces the file.\n    if directory?(path)\n      raise Errno::EISDIR, _(\"Is a directory: %{directory}\") % { directory: path }\n    end\n\n    super\n  end\nend\n","sourceCodeStart":2,"sourceCodeEnd":26,"githubUrl":"https://github.com/puppetlabs/puppet/blob/e227c27540975c25aa22d533a52424a9d2fc886a/lib/puppet/file_system/jruby.rb#L2-L26","documentation":"On JRuby (puppetserver), File.rename happily replaces a destination directory instead of failing like MRI Ruby. Puppet::FileSystem::JRuby#replace_file pre-checks directory?(path) and raises Errno::EISDIR with the path so both Rubies behave identically: atomic file replacement never silently deletes a directory.","triggerScenarios":"Calling Puppet::FileSystem.replace_file (directly or via Puppet APIs that write files atomically) where the destination path is an existing directory, under puppetserver/JRuby; usually a path collision between a planned file and an existing directory.","commonSituations":"A manifest or CA/inventory code path writing to a path that a previous run or package created as a directory; storing state under a path that is also a mount point; leftover directories from restored backups.","solutions":["Inspect the path: if it is a directory that is no longer needed, remove or relocate it before the write","Fix the writing code to target a file path that cannot collide with a directory (different name or subdirectory structure)","If the directory is required, change the design: write the file elsewhere and reference both explicitly"],"exampleFix":"# before\nPuppet::FileSystem.replace_file('/etc/app/state', 0o644) { |f| f.write(data) }\n# raises Errno::EISDIR on JRuby because /etc/app/state is a directory\n\n# after\nrequire 'fileutils'\nFileUtils.rm_r('/etc/app/state') if File.directory?('/etc/app/state')\nPuppet::FileSystem.replace_file('/etc/app/state', 0o644) { |f| f.write(data) }","handlingStrategy":"validation","validationCode":"if Puppet::FileSystem.directory?(path)\n  raise ArgumentError, \"refusing to replace_file a directory: #{path}\"\nend\nPuppet::FileSystem.replace_file(path, 0o644) { |f| f.write(data) }","typeGuard":"def writable_file_path?(path)\n  !Puppet::FileSystem.exist?(path) || Puppet::FileSystem.file?(path)\nend","tryCatchPattern":"begin\n  Puppet::FileSystem.replace_file(path, 0o644) { |f| f.write(data) }\nrescue Errno::EISDIR\n  raise \"#{path} is a directory; refusing to delete it automatically\"\nend","preventionTips":["Never let file and directory resources target the same path","On shared code paths, test the JRuby behavior (puppetserver) not just MRI","Clean up leftover directories after changing a path's intended type"],"tags":["puppet","file-system","jruby","eisdir","atomic-write"],"backgroundTag":"is-a-directory-error","analyzedSha":"e227c27540975c25aa22d533a52424a9d2fc886a","analyzedAt":"2026-08-21T20:49:46.650Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}