diff options
author | aamine <aamine@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2005-09-18 21:41:57 +0000 |
---|---|---|
committer | aamine <aamine@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2005-09-18 21:41:57 +0000 |
commit | b6c5814f2c4202111de6644636f7ffac4615610a (patch) | |
tree | 62bb3c0eacd7d94a23e44ff6472319d7bc912be1 /lib/fileutils.rb | |
parent | 8621cdd900a42c9ab7f0cd53cbc3ac94fb3f2f5d (diff) |
* lib/fileutils.rb: backported from trunk (rev 1.65):
* lib/fileutils.rb (rm_r): new option :secure.
* lib/fileutils.rb (rm_rf): new option :secure.
* lib/fileutils.rb: new method #remove_entry_secure.
* lib/fileutils.rb (cd): remove option :noop.
* lib/fileutils.rb (cp_r): new option :dereference_root.
* lib/fileutils.rb (cp_r): new option :dereference_root.
* lib/fileutils.rb: new method #remove_entry.
* lib/fileutils.rb: new method #chmod_R.
* lib/fileutils.rb: new method #chown.
* lib/fileutils.rb: new method #chown_R.
* lib/fileutils.rb: new method .commands.
* lib/fileutils.rb: new method .options.
* lib/fileutils.rb: new method .have_option?.
* lib/fileutils.rb: new method .options_of.
* lib/fileutils.rb: new method .collect_method.
* lib/fileutils.rb: use module_function instead of single extend.
* test/fileutils/test_fileutils.rb: backported from trunk (1.36).
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/branches/ruby_1_8@9217 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/fileutils.rb')
-rw-r--r-- | lib/fileutils.rb | 1208 |
1 files changed, 856 insertions, 352 deletions
diff --git a/lib/fileutils.rb b/lib/fileutils.rb index 931eabff8b..3465104d58 100644 --- a/lib/fileutils.rb +++ b/lib/fileutils.rb @@ -16,8 +16,11 @@ # cd(dir, options) {|dir| .... } # pwd() # mkdir(dir, options) +# mkdir(list, options) # mkdir_p(dir, options) +# mkdir_p(list, options) # rmdir(dir, options) +# rmdir(list, options) # ln(old, new, options) # ln(list, destdir, options) # ln_s(old, new, options) @@ -34,6 +37,9 @@ # rm_rf(list, options) # install(src, dest, mode = <src's>, options) # chmod(mode, list, options) +# chmod_R(mode, list, options) +# chown(user, group, list, options) +# chown_R(user, group, list, options) # touch(list, options) # # The <tt>options</tt> parameter is a hash of options, taken from the list @@ -45,13 +51,14 @@ # either one file or a list of files in that argument. See the method # documentation for examples. # -# There are some `low level' methods, which does not accept any option: +# There are some `low level' methods, which do not accept any option: # # copy_entry(src, dest, preserve = false, dereference = false) # copy_file(src, dest, preserve = false, dereference = true) # copy_stream(srcstream, deststream) +# remove_entry(path, force = false) +# remove_entry_secure(path, force = false) # remove_file(path, force = false) -# remove_dir(path, force = false) # compare_file(path_a, path_b) # compare_stream(stream_a, stream_b) # uptodate?(file, cmp_list) @@ -75,15 +82,15 @@ # <tt>:verbose</tt> flags to methods in FileUtils. # -unless defined?(Errno::EXDEV) - module Errno - class EXDEV < SystemCallError; end - end -end - module FileUtils - # All methods are module_function. + def self.private_module_function(name) #:nodoc: + module_function name + private_class_method name + end + + # This hash table holds command options. + OPT_TABLE = {} #:nodoc: internal use only # # Options: (none) @@ -93,11 +100,13 @@ module FileUtils def pwd Dir.pwd end + module_function :pwd alias getwd pwd + module_function :getwd # - # Options: noop verbose + # Options: verbose # # Changes the current directory to the directory +dir+. # @@ -107,13 +116,18 @@ module FileUtils # FileUtils.cd('/', :verbose => true) # chdir and report it # def cd(dir, options = {}, &block) # :yield: dir - fu_check_options options, :noop, :verbose + fu_check_options options, :verbose fu_output_message "cd #{dir}" if options[:verbose] Dir.chdir(dir, &block) unless options[:noop] fu_output_message 'cd -' if options[:verbose] and block end + module_function :cd alias chdir cd + module_function :chdir + + OPT_TABLE['cd'] = + OPT_TABLE['chdir'] = %w( verbose ) # # Options: (none) @@ -136,7 +150,7 @@ module FileUtils end true end - + module_function :uptodate? # # Options: mode noop verbose @@ -158,6 +172,9 @@ module FileUtils fu_mkdir dir, options[:mode] end end + module_function :mkdir + + OPT_TABLE['mkdir'] = %w( noop verbose mode ) # # Options: mode noop verbose @@ -206,11 +223,18 @@ module FileUtils return *list end + module_function :mkdir_p alias mkpath mkdir_p alias makedirs mkdir_p + module_function :mkpath + module_function :makedirs - def fu_mkdir(path, mode) + OPT_TABLE['mkdir_p'] = + OPT_TABLE['mkpath'] = + OPT_TABLE['makedirs'] = %w( noop verbose ) + + def fu_mkdir(path, mode) #:nodoc: path = path.sub(%r</\z>, '') if mode Dir.mkdir path, mode @@ -219,7 +243,7 @@ module FileUtils Dir.mkdir path end end - private :fu_mkdir + private_module_function :fu_mkdir # # Options: noop, verbose @@ -236,16 +260,18 @@ module FileUtils list = fu_list(list) fu_output_message "rmdir #{list.join ' '}" if options[:verbose] return if options[:noop] - list.each do |dir| Dir.rmdir dir.sub(%r</\z>, '') end end + module_function :rmdir + + OPT_TABLE['rmdir'] = %w( noop verbose ) # # Options: force noop verbose # - # <b><tt>ln( old, new, options = {} )</tt></b> + # <b><tt>ln(old, new, options = {})</tt></b> # # Creates a hard link +new+ which points to +old+. # If +new+ already exists and it is a directory, creates a symbolic link +new/old+. @@ -255,32 +281,36 @@ module FileUtils # FileUtils.ln 'gcc', 'cc', :verbose => true # FileUtils.ln '/usr/bin/emacs21', '/usr/bin/emacs' # - # <b><tt>ln( list, destdir, options = {} )</tt></b> + # <b><tt>ln(list, destdir, options = {})</tt></b> # # Creates several hard links in a directory, with each one pointing to the # item in +list+. If +destdir+ is not a directory, raises Errno::ENOTDIR. # # include FileUtils - # cd '/bin' - # ln %w(cp mv mkdir), '/usr/bin' # Now /usr/bin/cp and /bin/cp are linked. + # cd '/sbin' + # FileUtils.ln %w(cp mv mkdir), '/bin' # Now /sbin/cp and /bin/cp are linked. # def ln(src, dest, options = {}) fu_check_options options, :force, :noop, :verbose fu_output_message "ln#{options[:force] ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose] return if options[:noop] - fu_each_src_dest0(src, dest) do |s,d| remove_file d, true if options[:force] File.link s, d end end + module_function :ln alias link ln + module_function :link + + OPT_TABLE['ln'] = + OPT_TABLE['link'] = %w( noop verbose force ) # # Options: force noop verbose # - # <b><tt>ln_s( old, new, options = {} )</tt></b> + # <b><tt>ln_s(old, new, options = {})</tt></b> # # Creates a symbolic link +new+ which points to +old+. If +new+ already # exists and it is a directory, creates a symbolic link +new/old+. If +new+ @@ -290,7 +320,7 @@ module FileUtils # FileUtils.ln_s '/usr/bin/ruby', '/usr/local/bin/ruby' # FileUtils.ln_s 'verylongsourcefilename.c', 'c', :force => true # - # <b><tt>ln_s( list, destdir, options = {} )</tt></b> + # <b><tt>ln_s(list, destdir, options = {})</tt></b> # # Creates several symbolic links in a directory, with each one pointing to the # item in +list+. If +destdir+ is not a directory, raises Errno::ENOTDIR. @@ -303,14 +333,18 @@ module FileUtils fu_check_options options, :force, :noop, :verbose fu_output_message "ln -s#{options[:force] ? 'f' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose] return if options[:noop] - fu_each_src_dest0(src, dest) do |s,d| remove_file d, true if options[:force] File.symlink s, d end end + module_function :ln_s alias symlink ln_s + module_function :symlink + + OPT_TABLE['ln_s'] = + OPT_TABLE['symlink'] = %w( noop verbose force ) # # Options: noop verbose @@ -324,33 +358,41 @@ module FileUtils options[:force] = true ln_s src, dest, options end + module_function :ln_sf + + OPT_TABLE['ln_sf'] = %w( noop verbose ) # # Options: preserve noop verbose # - # Copies a file +src+ to +dest+. If +dest+ is a directory, copies - # +src+ to +dest/src+. + # Copies a file content +src+ to +dest+. If +dest+ is a directory, + # copies +src+ to +dest/src+. # # If +src+ is a list of files, then +dest+ must be a directory. # # FileUtils.cp 'eval.c', 'eval.c.org' # FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6' # FileUtils.cp %w(cgi.rb complex.rb date.rb), '/usr/lib/ruby/1.6', :verbose => true + # FileUtils.cp 'symlink', 'dest' # copy content, "dest" is not a symlink # def cp(src, dest, options = {}) fu_check_options options, :preserve, :noop, :verbose fu_output_message "cp#{options[:preserve] ? ' -p' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose] return if options[:noop] - - fu_each_src_dest(src, dest) do |s,d| + fu_each_src_dest(src, dest) do |s, d| copy_file s, d, options[:preserve] end end + module_function :cp alias copy cp + module_function :copy + + OPT_TABLE['cp'] = + OPT_TABLE['copy'] = %w( noop verbose preserve ) # - # Options: preserve noop verbose + # Options: preserve noop verbose dereference_root # # Copies +src+ to +dest+. If +src+ is a directory, this method copies # all its contents recursively. If +dest+ is a directory, copies @@ -373,61 +415,50 @@ module FileUtils # # but this doesn't. # def cp_r(src, dest, options = {}) - fu_check_options options, :preserve, :noop, :verbose + fu_check_options options, :preserve, :noop, :verbose, :dereference_root fu_output_message "cp -r#{options[:preserve] ? 'p' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose] return if options[:noop] - - fu_each_src_dest(src, dest) do |s,d| - if File.directory?(s) - fu_traverse(s) {|rel, deref, st| - ctx = CopyContext_.new(options[:preserve], deref, st) - ctx.copy_entry "#{s}/#{rel}", "#{d}/#{rel}" - } - else - copy_file s, d, options[:preserve] - end + fu_each_src_dest(src, dest) do |s, d| + copy_entry s, d, options[:preserve], options[:dereference_root] end end + module_function :cp_r - def fu_traverse(prefix, dereference_root = true) #:nodoc: - stack = ['.'] - deref = dereference_root - while rel = stack.pop - st = File.lstat("#{prefix}/#{rel}") - if st.directory? and (deref or not st.symlink?) - stack.concat Dir.entries("#{prefix}/#{rel}")\ - .reject {|ent| ent == '.' or ent == '..' }\ - .map {|ent| "#{rel}/#{ent.untaint}" }.reverse - end - yield rel, deref, st - deref = false - end - end - private :fu_traverse + OPT_TABLE['cp_r'] = %w( noop verbose preserve dereference_root ) # # Copies a file system entry +src+ to +dest+. - # This method preserves file types, c.f. FIFO, device files, directory.... + # If +src+ is a directory, this method copies its contents recursively. + # This method preserves file types, c.f. symlink, directory... + # (FIFO, device files and etc. are not supported yet) # # Both of +src+ and +dest+ must be a path name. # +src+ must exist, +dest+ must not exist. # # If +preserve+ is true, this method preserves owner, group, permissions # and modified time. - # If +dereference+ is true, this method copies a target of symbolic link - # instead of a symbolic link itself. # - def copy_entry(src, dest, preserve = false, dereference = false) - CopyContext_.new(preserve, dereference).copy_entry src, dest + # If +dereference_root+ is true, this method dereference tree root. + # + def copy_entry(src, dest, preserve = false, dereference_root = false) + Entry_.new(src, nil, dereference_root).traverse do |ent| + destent = Entry_.new(dest, ent.rel, false) + ent.copy destent.path + ent.copy_metadata destent.path if preserve + end end + module_function :copy_entry # # Copies file contents of +src+ to +dest+. # Both of +src+ and +dest+ must be a path name. # def copy_file(src, dest, preserve = false, dereference = true) - CopyContext_.new(preserve, dereference).copy_content src, dest + ent = Entry_.new(src, nil, dereference) + ent.copy_file dest + ent.copy_metadata dest if preserve end + module_function :copy_file # # Copies stream +src+ to +dest+. @@ -437,127 +468,7 @@ module FileUtils def copy_stream(src, dest) fu_copy_stream0 src, dest, fu_stream_blksize(src, dest) end - - def fu_copy_stream0(src, dest, blksize) #:nodoc: - while s = src.read(blksize) - dest.write s - end - end - private :fu_copy_stream0 - - class CopyContext_ #:nodoc: internal use only - include ::FileUtils - - def initialize(preserve = false, dereference = false, stat = nil) - @preserve = preserve - @dereference = dereference - @stat = stat - end - - def copy_entry(src, dest) - preserve(src, dest) { - _copy_entry src, dest - } - end - - def copy_content(src, dest) - preserve(src, dest) { - _copy_content src, dest - } - end - - private - - def _copy_entry(src, dest) - st = stat(src) - case - when st.file? - _copy_content src, dest - when st.directory? - begin - Dir.mkdir File.expand_path(dest) - rescue => err - raise unless File.directory?(dest) - end - when st.symlink? - File.symlink File.readlink(src), dest - when st.chardev? - raise "cannot handle device file" unless File.respond_to?(:mknod) - mknod dest, ?c, 0666, st.rdev - when st.blockdev? - raise "cannot handle device file" unless File.respond_to?(:mknod) - mknod dest, ?b, 0666, st.rdev - when st.socket? - raise "cannot handle socket" unless File.respond_to?(:mknod) - mknod dest, nil, st.mode, 0 - when st.pipe? - raise "cannot handle FIFO" unless File.respond_to?(:mkfifo) - mkfifo dest, 0666 - when (st.mode & 0xF000) == (_S_IF_DOOR = 0xD000) # door - raise "cannot handle door: #{src}" - else - raise "unknown file type: #{src}" - end - end - - def _copy_content(src, dest) - st = stat(src) - File.open(src, 'rb') {|r| - File.open(dest, 'wb', st.mode) {|w| - fu_copy_stream0 r, w, (fu_blksize(st) || fu_default_blksize()) - } - } - end - - def preserve(src, dest) - return yield unless @preserve - st = stat(src) - yield - File.utime st.atime, st.mtime, dest - begin - chown st.uid, st.gid, dest - rescue Errno::EPERM - # clear setuid/setgid - chmod st.mode & 01777, dest - else - chmod st.mode, dest - end - end - - def stat(path) - if @dereference - @stat ||= ::File.stat(path) - else - @stat ||= ::File.lstat(path) - end - end - - def chmod(mode, path) - if @dereference - ::File.chmod mode, path - else - begin - ::File.lchmod mode, path - rescue NotImplementedError - # just ignore this because chmod(symlink) changes attributes of - # symlink target, which is not our intent. - end - end - end - - def chown(uid, gid, path) - if @dereference - ::File.chown uid, gid, path - else - begin - ::File.lchown uid, gid, path - rescue NotImplementedError - # just ignore this because chown(symlink) changes attributes of - # symlink target, which is not our intent. - end - end - end - end + module_function :copy_stream # # Options: force noop verbose @@ -575,16 +486,15 @@ module FileUtils fu_check_options options, :force, :noop, :verbose fu_output_message "mv#{options[:force] ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose] return if options[:noop] - - fu_each_src_dest(src, dest) do |s,d| - src_stat = fu_lstat(s) - dest_stat = fu_stat(d) + fu_each_src_dest(src, dest) do |s, d| + destent = Entry_.new(d, nil, true) begin - if rename_cannot_overwrite_file? and dest_stat and not dest_stat.directory? - File.unlink d - end - if dest_stat and dest_stat.directory? - raise Errno::EISDIR, dest + if destent.exist? + if destent.directory? + raise Errno::EEXIST, dest + else + destent.remove_file if rename_cannot_overwrite_file? + end end begin File.rename s, d @@ -596,27 +506,18 @@ module FileUtils end end end + module_function :mv alias move mv + module_function :move - def fu_stat(path) - File.stat(path) - rescue SystemCallError - nil - end - private :fu_stat - - def fu_lstat(path) - File.lstat(path) - rescue SystemCallError - nil - end - private :fu_lstat + OPT_TABLE['mv'] = + OPT_TABLE['move'] = %w( noop verbose force ) def rename_cannot_overwrite_file? #:nodoc: /djgpp|cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM end - private :rename_cannot_overwrite_file? + private_module_function :rename_cannot_overwrite_file? # # Options: force noop verbose @@ -634,18 +535,24 @@ module FileUtils fu_output_message "rm#{options[:force] ? ' -f' : ''} #{list.join ' '}" if options[:verbose] return if options[:noop] - list.each do |fname| - remove_file fname, options[:force] + list.each do |path| + remove_file path, options[:force] end end + module_function :rm alias remove rm + module_function :remove + + OPT_TABLE['rm'] = + OPT_TABLE['remove'] = %w( noop verbose force ) # # Options: noop verbose # - # Same as - # #rm(list, :force) + # Equivalent to + # + # #rm(list, :force => true) # def rm_f(list, options = {}) fu_check_options options, :noop, :verbose @@ -653,11 +560,16 @@ module FileUtils options[:force] = true rm list, options end + module_function :rm_f alias safe_unlink rm_f + module_function :safe_unlink + + OPT_TABLE['rm_f'] = + OPT_TABLE['safe_unlink'] = %w( noop verbose ) # - # Options: force noop verbose + # Options: force noop verbose secure # # remove files +list+[0] +list+[1]... If +list+[n] is a directory, # removes its all contents recursively. This method ignores @@ -665,94 +577,200 @@ module FileUtils # # FileUtils.rm_r Dir.glob('/tmp/*') # FileUtils.rm_r '/', :force => true # :-) + # + # WARNING: This method causes local vulnerability + # if one of parent directories or removing directory tree are world + # writable (including /tmp, whose permission is 1777), and the current + # process has strong privilege such as Unix super user (root), and the + # system has symbolic link. For secure removing, read the documentation + # of #remove_entry_secure carefully, and set :secure option to true. + # Default is :secure=>false. + # + # NOTE: This method calls #remove_entry_secure if :secure option is set. + # See also #remove_entry_secure. # def rm_r(list, options = {}) - fu_check_options options, :force, :noop, :verbose + fu_check_options options, :force, :noop, :verbose, :secure + # options[:secure] = true unless options.key?(:secure) list = fu_list(list) fu_output_message "rm -r#{options[:force] ? 'f' : ''} #{list.join ' '}" if options[:verbose] return if options[:noop] - - list.each do |fname| - begin - st = File.lstat(fname) - rescue - next if options[:force] - raise - end - if st.symlink? then remove_file fname, options[:force] - elsif st.directory? then remove_dir fname, options[:force] - else remove_file fname, options[:force] + list.each do |path| + if options[:secure] + remove_entry_secure path, options[:force] + else + remove_entry path, options[:force] end end end + module_function :rm_r + + OPT_TABLE['rm_r'] = %w( noop verbose force secure ) # - # Options: noop verbose + # Options: noop verbose secure # - # Same as + # Equivalent to + # # #rm_r(list, :force => true) + # + # WARNING: This method causes local vulnerability. + # Read the documentation of #rm_r first. # def rm_rf(list, options = {}) - fu_check_options options, :noop, :verbose + fu_check_options options, :noop, :verbose, :secure options = options.dup options[:force] = true rm_r list, options end + module_function :rm_rf alias rmtree rm_rf + module_function :rmtree + + OPT_TABLE['rm_rf'] = + OPT_TABLE['rmtree'] = %w( noop verbose secure ) + + # + # This method removes a file system entry +path+. +path+ shall be a + # regular file, a directory, or something. If +path+ is a directory, + # remove it recursively. This method is required to avoid TOCTTOU + # (time-of-check-to-time-of-use) local security vulnerability of #rm_r. + # #rm_r causes security hole when: + # + # * Parent directory is world writable (including /tmp). + # * Removing directory tree includes world writable directory. + # * The system has symbolic link. + # + # To avoid this security hole, this method applies special preprocess. + # If +path+ is a directory, this method chown(2) and chmod(2) all + # removing directories. This requires the current process is the + # owner of the removing whole directory tree, or is the super user (root). + # + # WARNING: You must ensure that *ALL* parent directories are not + # world writable. Otherwise this method does not work. + # Only exception is temporary directory like /tmp and /var/tmp, + # whose permission is 1777. + # + # WARNING: Only the owner of the removing directory tree, or Unix super + # user (root) should invoke this method. Otherwise this method does not + # work. + # + # For details of this security vulnerability, see Perl's case: + # + # http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448 + # http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452 + # + # For fileutils.rb, this vulnerability is reported in [ruby-dev:26100]. + # + def remove_entry_secure(path, force = false) + unless fu_have_symlink? + remove_entry path, force + return + end + fullpath = File.expand_path(path) + st = File.lstat(fullpath) + unless st.directory? + File.unlink fullpath + return + end + # is a directory. + parent_st = File.stat(File.dirname(fullpath)) + unless fu_world_writable?(parent_st) + remove_entry path, force + return + end + unless parent_st.sticky? + raise ArgumentError, "parent directory is world writable, FileUtils#remove_entry_secure does not work; abort: #{path.inspect} (parent directory mode #{'%o' % parent_st.mode})" + end + # freeze tree root + euid = Process.euid + File.open(fullpath + '/.') {|f| + unless fu_stat_identical_entry?(st, f.stat) + # symlink (TOC-to-TOU attack?) + File.unlink fullpath + return + end + f.chown euid, -1 + f.chmod 0700 + } + # ---- tree root is frozen ---- + root = Entry_.new(path) + root.preorder_traverse do |ent| + if ent.directory? + ent.chown euid, -1 + ent.chmod 0700 + end + end + root.postorder_traverse do |ent| + begin + ent.remove + rescue + raise unless force + end + end + rescue + raise unless force + end + module_function :remove_entry_secure + + def fu_world_writable?(st) + (st.mode & 0002) != 0 + end + + def fu_have_symlink? #:nodoc + File.symlink nil, nil + rescue NotImplementedError + return false + rescue + return true + end + private_module_function :fu_have_symlink? + + def fu_stat_identical_entry?(a, b) #:nodoc: + a.dev == b.dev and a.ino == b.ino + end + private_module_function :fu_stat_identical_entry? + + # + # This method removes a file system entry +path+. + # +path+ might be a regular file, a directory, or something. + # If +path+ is a directory, remove it recursively. + # + # See also #remove_entry_secure. + # + def remove_entry(path, force = false) + Entry_.new(path).postorder_traverse do |ent| + begin + ent.remove + rescue + raise unless force + end + end + rescue + raise unless force + end + module_function :remove_entry + # # Removes a file +path+. # This method ignores StandardError if +force+ is true. + # def remove_file(path, force = false) - first_time_p = true - begin - File.unlink path - rescue Errno::ENOENT - raise unless force - rescue => err - if first_time_p - first_time_p = false - begin - File.chmod 0777, path - retry - rescue SystemCallError - end - end - raise err unless force - end + Entry_.new(path).remove_file + rescue + raise unless force end + module_function :remove_file + # # Removes a directory +dir+ and its contents recursively. # This method ignores StandardError if +force+ is true. - def remove_dir(dir, force = false) - Dir.foreach(dir) do |file| - next if /\A\.\.?\z/ =~ file - path = "#{dir}/#{file.untaint}" - if File.symlink?(path) - remove_file path, force - elsif File.directory?(path) - remove_dir path, force - else - remove_file path, force - end - end - first_time_p = true - begin - Dir.rmdir dir.sub(%r</\z>, '') - rescue Errno::ENOENT - raise unless force - rescue => err - if first_time_p - first_time_p = false - begin - File.chmod 0777, dir - retry - rescue SystemCallError - end - end - raise err unless force - end + # + def remove_dir(path, force = false) + remove_entry path, force # FIXME?? check if it is a directory end + module_function :remove_dir # # Returns true if the contents of a file A and a file B are identical. @@ -768,9 +786,12 @@ module FileUtils } } end + module_function :compare_file alias identical? compare_file alias cmp compare_file + module_function :identical? + module_function :cmp # # Returns true if the contents of a stream +a+ and +b+ are identical. @@ -789,6 +810,7 @@ module FileUtils end false end + module_function :compare_stream # # Options: mode noop verbose @@ -803,9 +825,8 @@ module FileUtils fu_check_options options, :mode, :preserve, :noop, :verbose fu_output_message "install -c#{options[:preserve] && ' -p'}#{options[:mode] ? (' -m 0%o' % options[:mode]) : ''} #{[src,dest].flatten.join ' '}" if options[:verbose] return if options[:noop] - - fu_each_src_dest(src, dest) do |s,d| - unless File.exist?(d) and compare_file(s,d) + fu_each_src_dest(src, dest) do |s, d| + unless File.exist?(d) and compare_file(s, d) remove_file d, true st = File.stat(s) if options[:preserve] copy_file s, d @@ -814,6 +835,9 @@ module FileUtils end end end + module_function :install + + OPT_TABLE['install'] = %w( noop verbose preserve mode ) # # Options: noop verbose @@ -830,7 +854,143 @@ module FileUtils list = fu_list(list) fu_output_message sprintf('chmod %o %s', mode, list.join(' ')) if options[:verbose] return if options[:noop] - File.chmod mode, *list + list.each do |path| + Entry_.new(path).chmod mode + end + end + module_function :chmod + + OPT_TABLE['chmod'] = %w( noop verbose ) + + # + # Options: noop verbose force + # + # Changes permission bits on the named files (in +list+) + # to the bit pattern represented by +mode+. + # + # FileUtils.chmod_R 0700, "/tmp/app.#{$$}" + # + def chmod_R(mode, list, options = {}) + fu_check_options options, :noop, :verbose, :force + list = fu_list(list) + fu_output_message sprintf('chmod -R%s %o %s', + (options[:force] ? 'f' : ''), + mode, list.join(' ')) if options[:verbose] + return if options[:noop] + list.each do |root| + Entry_.new(root).traverse do |ent| + begin + ent.chmod mode + rescue + raise unless options[:force] + end + end + end + end + module_function :chmod_R + + OPT_TABLE['chmod_R'] = %w( noop verbose ) + + # + # Options: noop verbose + # + # Changes owner and group on the named files (in +list+) + # to the user +user+ and the group +group+. +user+ and +group+ + # may be an ID (Integer/String) or a name (String). + # If +user+ or +group+ is nil, this method does not change + # the attribute. + # + # FileUtils.chown 'root', 'staff', '/usr/local/bin/ruby' + # FileUtils.chown nil, 'bin', Dir.glob('/usr/bin/*'), :verbose => true + # + def chown(user, group, list, options = {}) + fu_check_options options, :noop, :verbose + list = fu_list(list) + fu_output_message sprintf('chown %s%s', + [user,group].compact.join(':') + ' ', + list.join(' ')) if options[:verbose] + return if options[:noop] + uid = fu_get_uid(user) + gid = fu_get_gid(group) + list.each do |path| + Entry_.new(path).chown uid, gid + end + end + module_function :chown + + OPT_TABLE['chown'] = %w( noop verbose ) + + # + # Options: noop verbose force + # + # Changes owner and group on the named files (in +list+) + # to the user +user+ and the group +group+ recursively. + # +user+ and +group+ may be an ID (Integer/String) or + # a name (String). If +user+ or +group+ is nil, this + # method does not change the attribute. + # + # FileUtils.chown_R 'www', 'www', '/var/www/htdocs' + # FileUtils.chown_R 'cvs', 'cvs', '/var/cvs', :verbose => true + # + def chown_R(user, group, list, options = {}) + fu_check_options options, :noop, :verbose, :force + list = fu_list(list) + fu_output_message sprintf('chown -R%s %s%s', + (options[:force] ? 'f' : ''), + [user,group].compact.join(':') + ' ', + list.join(' ')) if options[:verbose] + return if options[:noop] + uid = fu_get_uid(user) + gid = fu_get_gid(group) + return unless uid or gid + list.each do |root| + Entry_.new(root).traverse do |ent| + begin + ent.chown uid, gid + rescue + raise unless options[:force] + end + end + end + end + module_function :chown_R + + OPT_TABLE['chown_R'] = %w( noop verbose ) + + begin + require 'etc' + + def fu_get_uid(user) #:nodoc: + return nil unless user + user = user.to_s + if /\A\d+\z/ =~ user + then user.to_i + else Etc.getpwnam(user).uid + end + end + private_module_function :fu_get_uid + + def fu_get_gid(group) #:nodoc: + return nil unless group + if /\A\d+\z/ =~ group + then group.to_i + else Etc.getgrnam(group).gid + end + end + private_module_function :fu_get_gid + + rescue LoadError + # need Win32 support??? + + def fu_get_uid(user) #:nodoc: + user # FIXME + end + private_module_function :fu_get_uid + + def fu_get_gid(group) #:nodoc: + group # FIXME + end + private_module_function :fu_get_gid end # @@ -847,56 +1007,396 @@ module FileUtils list = fu_list(list) fu_output_message "touch #{list.join ' '}" if options[:verbose] return if options[:noop] - t = Time.now - list.each do |fname| + list.each do |path| begin - File.utime(t, t, fname) + File.utime(t, t, path) rescue Errno::ENOENT - File.open(fname, 'a') { + File.open(path, 'a') { ; } end end end + module_function :touch + + OPT_TABLE['touch'] = %w( noop verbose ) private - def fu_check_options(options, *optdecl) - h = options.dup - optdecl.each do |name| - h.delete name + module StreamUtils_ + private + + def fu_windows? + /mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM + end + + def fu_copy_stream0(src, dest, blksize) #:nodoc: + # FIXME: readpartial? + while s = src.read(blksize) + dest.write s + end + end + + def fu_stream_blksize(*streams) + streams.each do |s| + next unless s.respond_to?(:stat) + size = fu_blksize(s.stat) + return size if size + end + fu_default_blksize() + end + + def fu_blksize(st) + s = st.blksize + return nil unless s + return nil if s == 0 + s + end + + def fu_default_blksize + 1024 end - raise ArgumentError, "no such option: #{h.keys.join(' ')}" unless h.empty? end - def fu_list(arg) + include StreamUtils_ + extend StreamUtils_ + + class Entry_ #:nodoc: internal use only + include StreamUtils_ + + def initialize(a, b = nil, deref = false) + @prefix = @rel = @path = nil + if b + @prefix = a + @rel = b + else + @path = a + end + @deref = deref + @stat = nil + @lstat = nil + end + + def inspect + "\#<#{self.class} #{path()}>" + end + + def path + if @path + @path.to_str + else + join(@prefix, @rel) + end + end + + def prefix + @prefix || @path + end + + def rel + @rel + end + + def dereference? + @deref + end + + def exist? + lstat! ? true : false + end + + def file? + s = lstat! + s and s.file? + end + + def directory? + s = lstat! + s and s.directory? + end + + def symlink? + s = lstat! + s and s.symlink? + end + + def chardev? + s = lstat! + s and s.chardev? + end + + def blockdev? + s = lstat! + s and s.blockdev? + end + + def socket? + s = lstat! + s and s.socket? + end + + def pipe? + s = lstat! + s and s.pipe? + end + + S_IF_DOOR = 0xD000 + + def door? + s = lstat! + s and (s.mode & 0xF000 == S_IF_DOOR) + end + + def entries + Dir.entries(path())\ + .reject {|n| n == '.' or n == '..' }\ + .map {|n| Entry_.new(prefix(), join(rel(), n.untaint)) } + end + + def stat + return @stat if @stat + if lstat() and lstat().symlink? + @stat = File.stat(path()) + else + @stat = lstat() + end + @stat + end + + def stat! + return @stat if @stat + if lstat! and lstat!.symlink? + @stat = File.stat(path()) + else + @stat = lstat! + end + @stat + rescue SystemCallError + nil + end + + def lstat + if dereference? + @lstat ||= File.stat(path()) + else + @lstat ||= File.lstat(path()) + end + end + + def lstat! + lstat() + rescue SystemCallError + nil + end + + def chmod(mode) + if symlink? + File.lchmod mode, path() if have_lchmod? + else + File.chmod mode, path() + end + end + + def chown(uid, gid) + if symlink? + File.lchown uid, gid, path() if have_lchown? + else + File.chown uid, gid, path() + end + end + + def copy(dest) + case + when file? + copy_file dest + when directory? + begin + Dir.mkdir dest + rescue + raise unless File.directory?(dest) + end + when symlink? + File.symlink File.readlink(path()), dest + when chardev? + raise "cannot handle device file" unless File.respond_to?(:mknod) + mknod dest, ?c, 0666, lstat().rdev + when blockdev? + raise "cannot handle device file" unless File.respond_to?(:mknod) + mknod dest, ?b, 0666, lstat().rdev + when socket? + raise "cannot handle socket" unless File.respond_to?(:mknod) + mknod dest, nil, lstat().mode, 0 + when pipe? + raise "cannot handle FIFO" unless File.respond_to?(:mkfifo) + mkfifo dest, 0666 + when door? + raise "cannot handle door: #{path()}" + else + raise "unknown file type: #{path()}" + end + end + + def copy_file(dest) + st = stat() + File.open(path(), 'rb') {|r| + File.open(dest, 'wb', st.mode) {|w| + fu_copy_stream0 r, w, (fu_blksize(st) || fu_default_blksize()) + } + } + end + + def copy_metadata(path) + st = lstat() + File.utime st.atime, st.mtime, path + begin + File.chown st.uid, st.gid, path + rescue Errno::EPERM + # clear setuid/setgid + File.chmod st.mode & 01777, path + else + File.chmod st.mode, path + end + end + + def remove + if directory? + remove_dir1 + else + remove_file + end + end + + def remove_dir1 + platform_support { + Dir.rmdir path().sub(%r</\z>, '') + } + end + + def remove_file + platform_support { + File.unlink path + } + end + + def platform_support + return yield unless fu_windows? + first_time_p = true + begin + yield + rescue Errno::ENOENT + raise + rescue => err + if first_time_p + first_time_p = false + begin + File.chmod 0700, path() # Windows does not have symlink + retry + rescue SystemCallError + end + end + raise err + end + end + + def preorder_traverse + stack = [self] + while ent = stack.pop + yield ent + stack.concat ent.entries.reverse if ent.directory? + end + end + + alias traverse preorder_traverse + + def postorder_traverse + if directory? + entries().each do |ent| + ent.postorder_traverse do |e| + yield e + end + end + end + yield self + end + + private + + $fileutils_rb_have_lchmod = nil + + def have_lchmod? + # This is not MT-safe, but it does not matter. + if $fileutils_rb_have_lchmod == nil + $fileutils_rb_have_lchmod = check_have_lchmod? + end + $fileutils_rb_have_lchmod + end + + def check_have_lchmod? + return false unless File.respond_to?(:lchmod) + File.lchmod 0 + return true + rescue NotImplementedError + return false + end + + $fileutils_rb_have_lchown = nil + + def have_lchown? + # This is not MT-safe, but it does not matter. + if $fileutils_rb_have_lchown == nil + $fileutils_rb_have_lchown = check_have_lchown? + end + $fileutils_rb_have_lchown + end + + def check_have_lchown? + return false unless File.respond_to?(:lchown) + File.lchown nil, nil + return true + rescue NotImplementedError + return false + end + + def join(dir, base) + return dir.to_str if not base or base == '.' + return base.to_str if not dir or dir == '.' + File.join(dir, base) + end + end # class Entry_ + + def fu_list(arg) #:nodoc: [arg].flatten.map {|path| path.to_str } end + private_module_function :fu_list - def fu_each_src_dest(src, dest) + def fu_each_src_dest(src, dest) #:nodoc: fu_each_src_dest0(src, dest) do |s, d| raise ArgumentError, "same file: #{s} and #{d}" if fu_same?(s, d) yield s, d end end + private_module_function :fu_each_src_dest - def fu_each_src_dest0(src, dest) + def fu_each_src_dest0(src, dest) #:nodoc: if src.is_a?(Array) src.each do |s| - yield s.to_str, File.join(dest, File.basename(s)) + s = s.to_str + yield s, File.join(dest, File.basename(s)) end else + src = src.to_str if File.directory?(dest) - yield src.to_str, File.join(dest, File.basename(src)) + yield src, File.join(dest, File.basename(src)) else - yield src.to_str, dest.to_str + yield src, dest.to_str end end end + private_module_function :fu_each_src_dest0 - def fu_same?(a, b) - if have_st_ino? + def fu_same?(a, b) #:nodoc: + if fu_have_st_ino? st1 = File.stat(a) st2 = File.stat(b) st1.dev == st2.dev and st1.ino == st2.ino @@ -906,82 +1406,89 @@ module FileUtils rescue Errno::ENOENT return false end + private_module_function :fu_same? - def have_st_ino? - /mswin|mingw|bccwin|wince|emx/ !~ RUBY_PLATFORM + def fu_have_st_ino? #:nodoc: + not fu_windows? end + private_module_function :fu_have_st_ino? - def fu_stream_blksize(*streams) - streams.each do |s| - next unless s.respond_to?(:stat) - size = fu_blksize(s.stat) - return size if size + def fu_check_options(options, *optdecl) #:nodoc: + h = options.dup + optdecl.each do |name| + h.delete name end - fu_default_blksize() - end - - def fu_blksize(st) - s = st.blksize - return nil unless s - return nil if s == 0 - s + raise ArgumentError, "no such option: #{h.keys.join(' ')}" unless h.empty? end + private_module_function :fu_check_options - def fu_default_blksize - 1024 + def fu_update_option(args, new) #:nodoc: + if args.last.is_a?(Hash) + args[-1] = args.last.dup.update(new) + else + args.push new + end + args end + private_module_function :fu_update_option @fileutils_output = $stderr @fileutils_label = '' - def fu_output_message(msg) + def fu_output_message(msg) #:nodoc: @fileutils_output ||= $stderr @fileutils_label ||= '' @fileutils_output.puts @fileutils_label + msg end + private_module_function :fu_output_message - def fu_update_option(args, new) - if args.last.is_a?(Hash) - args.last.update new - else - args.push new - end - args + # + # Returns an Array of method names which have any options. + # + # p FileUtils.commands #=> ["chmod", "cp", "cp_r", "install", ...] + # + def FileUtils.commands + OPT_TABLE.keys end + # + # Returns an Array of option names. + # + # p FileUtils.options #=> ["noop", "force", "verbose", "preserve", "mode"] + # + def FileUtils.options + OPT_TABLE.values.flatten.uniq + end - extend self - - - OPT_TABLE = { - 'cd' => %w( noop verbose ), - 'chdir' => %w( noop verbose ), - 'chmod' => %w( noop verbose ), - 'copy' => %w( noop verbose preserve ), - 'cp' => %w( noop verbose preserve ), - 'cp_r' => %w( noop verbose preserve ), - 'install' => %w( noop verbose preserve mode ), - 'link' => %w( noop verbose force ), - 'ln' => %w( noop verbose force ), - 'ln_s' => %w( noop verbose force ), - 'ln_sf' => %w( noop verbose ), - 'makedirs' => %w( noop verbose ), - 'mkdir' => %w( noop verbose mode ), - 'mkdir_p' => %w( noop verbose mode ), - 'mkpath' => %w( noop verbose ), - 'move' => %w( noop verbose force ), - 'mv' => %w( noop verbose force ), - 'remove' => %w( noop verbose force ), - 'rm' => %w( noop verbose force ), - 'rm_f' => %w( noop verbose ), - 'rm_r' => %w( noop verbose force ), - 'rm_rf' => %w( noop verbose ), - 'rmtree' => %w( noop verbose ), - 'rmdir' => %w( noop verbose ), - 'safe_unlink' => %w( noop verbose ), - 'symlink' => %w( noop verbose force ), - 'touch' => %w( noop verbose ) - } + # + # Returns true if the method +mid+ have an option +opt+. + # + # p FileUtils.have_option?(:cp, :noop) #=> true + # p FileUtils.have_option?(:rm, :force) #=> true + # p FileUtils.have_option?(:rm, :perserve) #=> false + # + def FileUtils.have_option?(mid, opt) + li = OPT_TABLE[mid.to_s] or raise ArgumentError, "no such method: #{mid}" + li.include?(opt.to_s) + end + + # + # Returns an Array of option names of the method +mid+. + # + # p FileUtils.options(:rm) #=> ["noop", "verbose", "force"] + # + def FileUtils.options_of(mid) + OPT_TABLE[mid.to_s] + end + + # + # Returns an Array of method names which have the option +opt+. + # + # p FileUtils.collect_method(:preserve) #=> ["cp", "cp_r", "copy", "install"] + # + def FileUtils.collect_method(opt) + OPT_TABLE.keys.select {|m| OPT_TABLE[m].include?(opt.to_s) } + end # # This module has all methods of FileUtils module, but it outputs messages @@ -992,8 +1499,7 @@ module FileUtils include FileUtils @fileutils_output = $stderr @fileutils_label = '' - FileUtils::OPT_TABLE.each do |name, opts| - next unless opts.include?('verbose') + ::FileUtils.collect_method('verbose').each do |name| module_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{name}(*args) super(*fu_update_option(args, :verbose => true)) @@ -1012,8 +1518,7 @@ module FileUtils include FileUtils @fileutils_output = $stderr @fileutils_label = '' - FileUtils::OPT_TABLE.each do |name, opts| - next unless opts.include?('noop') + ::FileUtils.collect_method('noop').each do |name| module_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{name}(*args) super(*fu_update_option(args, :noop => true)) @@ -1033,8 +1538,7 @@ module FileUtils include FileUtils @fileutils_output = $stderr @fileutils_label = '' - FileUtils::OPT_TABLE.each do |name, opts| - next unless opts.include?('noop') + ::FileUtils.collect_method('noop').each do |name| module_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{name}(*args) super(*fu_update_option(args, :noop => true, :verbose => true)) |