diff --git a/spec/std/file_spec.cr b/spec/std/file_spec.cr index 2c7d38627392..f48e81e70514 100644 --- a/spec/std/file_spec.cr +++ b/spec/std/file_spec.cr @@ -224,11 +224,13 @@ describe "File" do describe "link" do it "creates a hard link" do + in_path = "#{__DIR__}/data/test_file.txt" out_path = "#{__DIR__}/data/test_file_link.txt" begin - File.link("#{__DIR__}/data/test_file.txt", out_path) + File.link(in_path, out_path) File.exists?(out_path).should be_true File.symlink?(out_path).should be_false + File.info(in_path).should eq(File.info(out_path)) ensure File.delete(out_path) if File.exists?(out_path) end @@ -237,10 +239,12 @@ describe "File" do describe "symlink" do it "creates a symbolic link" do + in_path = "#{__DIR__}/data/test_file.txt" out_path = "#{__DIR__}/data/test_file_symlink.txt" begin - File.symlink("#{__DIR__}/data/test_file.txt", out_path) + File.symlink(in_path, out_path) File.symlink?(out_path).should be_true + File.info(in_path).should eq(File.info(out_path)) ensure File.delete(out_path) if File.exists?(out_path) end @@ -325,7 +329,7 @@ describe "File" do begin File.write(path, "") File.chmod(path, 0o775) - File.stat(path).perm.should eq(0o775) + File.info(path).permissions.should eq(File::Permissions.new(0o775)) ensure File.delete(path) if File.exists?(path) end @@ -336,12 +340,23 @@ describe "File" do begin Dir.mkdir(path, 0o775) File.chmod(path, 0o664) - File.stat(path).perm.should eq(0o664) + File.info(path).permissions.should eq(File::Permissions.new(0o664)) ensure Dir.rmdir(path) if Dir.exists?(path) end end + it "can take File::Permissions" do + path = "#{__DIR__}/data/chmod.txt" + begin + File.write(path, "") + File.chmod(path, File::Permissions.flags(OwnerAll, GroupAll, OtherExecute, OtherRead)) + File.info(path).permissions.should eq(File::Permissions.new(0o775)) + ensure + File.delete(path) if File.exists?(path) + end + end + it "follows symlinks" do path = "#{__DIR__}/data/chmod_destination.txt" link = "#{__DIR__}/data/chmod.txt" @@ -349,7 +364,7 @@ describe "File" do File.write(path, "") File.symlink(path, link) File.chmod(link, 0o775) - File.stat(link).perm.should eq(0o775) + File.info(link).permissions.should eq(File::Permissions.new(0o775)) ensure File.delete(path) if File.exists?(path) File.delete(link) if File.symlink?(link) @@ -363,83 +378,74 @@ describe "File" do end end - it "gets stat for this file" do - stat = File.stat(__FILE__) - stat.blockdev?.should be_false - stat.chardev?.should be_false - stat.directory?.should be_false - stat.file?.should be_true - stat.symlink?.should be_false - stat.socket?.should be_false + it "gets info for this file" do + info = File.info(__FILE__) + info.type.should eq(File::Type::File) end - it "gets stat for this directory" do - stat = File.stat(__DIR__) - stat.blockdev?.should be_false - stat.chardev?.should be_false - stat.directory?.should be_true - stat.file?.should be_false - stat.symlink?.should be_false - stat.socket?.should be_false + it "gets info for this directory" do + info = File.info(__DIR__) + info.type.should eq(File::Type::Directory) end - it "gets stat for a character device" do - stat = File.stat("/dev/null") - stat.blockdev?.should be_false - stat.chardev?.should be_true - stat.directory?.should be_false - stat.file?.should be_false - stat.symlink?.should be_false - stat.socket?.should be_false + it "gets info for a character device" do + info = File.info("/dev/null") + info.type.should eq(File::Type::CharacterDevice) end - it "gets stat for a symlink" do - stat = File.lstat("#{__DIR__}/data/symlink.txt") - stat.blockdev?.should be_false - stat.chardev?.should be_false - stat.directory?.should be_false - stat.file?.should be_false - stat.symlink?.should be_true - stat.socket?.should be_false + it "gets info for a symlink" do + info = File.info("#{__DIR__}/data/symlink.txt", follow_symlinks: false) + info.type.should eq(File::Type::Symlink) end - it "gets stat for open file" do + it "gets info for open file" do File.open(__FILE__, "r") do |file| - stat = file.stat - stat.blockdev?.should be_false - stat.chardev?.should be_false - stat.directory?.should be_false - stat.file?.should be_true - stat.symlink?.should be_false - stat.socket?.should be_false - stat.pipe?.should be_false + info = file.info + info.type.should eq(File::Type::File) end end - it "gets stat for pipe" do + it "gets info for pipe" do IO.pipe do |r, w| - r.stat.pipe?.should be_true - w.stat.pipe?.should be_true + r.info.type.should eq(File::Type::Pipe) + w.info.type.should eq(File::Type::Pipe) end end - it "gets stat for non-existent file and raises" do + it "gets info for non-existent file and raises" do expect_raises Errno do - File.stat("non-existent") + File.info("non-existent") end end - it "gets stat mtime for new file" do + it "gets info mtime for new file" do tmp = Tempfile.new "tmp" begin - (tmp.stat.atime - Time.utc_now).total_seconds.should be < 5 - (tmp.stat.ctime - Time.utc_now).total_seconds.should be < 5 - (tmp.stat.mtime - Time.utc_now).total_seconds.should be < 5 + tmp.info.modification_time.should be_close(Time.now, 5.seconds) + File.info(tmp.path).modification_time.should be_close(Time.now, 5.seconds) ensure tmp.delete end end + describe "File::Info" do + it "tests equal for the same file" do + File.info(__FILE__).should eq(File.info(__FILE__)) + end + + it "tests equal for the same directory" do + File.info(__DIR__).should eq(File.info(__DIR__)) + end + + it "tests unequal for different files" do + File.info(__FILE__).should_not eq(File.info("#{__DIR__}/data/test_file.txt")) + end + + it "tests unequal for file and directory" do + File.info(__DIR__).should_not eq(File.info("#{__DIR__}/data/test_file.txt")) + end + end + describe "size" do it { File.size("#{__DIR__}/data/test_file.txt").should eq(240) } it do @@ -713,11 +719,20 @@ describe "File" do end end - it "opens with perm" do + it "opens with perm (int)" do filename = "#{__DIR__}/data/temp_write.txt" perm = 0o600 File.open(filename, "w", perm) do |file| - file.stat.perm.should eq(perm) + file.info.permissions.should eq(File::Permissions.new(perm)) + end + File.delete filename + end + + it "opens with perm (File::Permissions)" do + filename = "#{__DIR__}/data/temp_write.txt" + perm = File::Permissions.flags(OwnerRead, OwnerWrite) + File.open(filename, "w", perm) do |file| + file.info.permissions.should eq(perm) end File.delete filename end @@ -920,12 +935,12 @@ describe "File" do File.rename("baz", "foo\0bar") end - it_raises_on_null_byte "stat" do - File.stat("foo\0bar") + it_raises_on_null_byte "info" do + File.info("foo\0bar") end - it_raises_on_null_byte "lstat" do - File.lstat("foo\0bar") + it_raises_on_null_byte "info?" do + File.info?("foo\0bar") end it_raises_on_null_byte "exists?" do @@ -1071,9 +1086,8 @@ describe "File" do File.utime(atime, mtime, filename) - stat = File.stat(filename) - stat.atime.should eq(atime) - stat.mtime.should eq(mtime) + info = File.info(filename) + info.modification_time.should eq(mtime) File.delete filename end @@ -1106,9 +1120,8 @@ describe "File" do begin File.touch(filename, time) - stat = File.stat(filename) - stat.atime.should eq(time) - stat.mtime.should eq(time) + info = File.info(filename) + info.modification_time.should eq(time) ensure File.delete filename end @@ -1120,9 +1133,8 @@ describe "File" do begin File.touch(filename) - stat = File.stat(filename) - stat.atime.should be_close(time, 1.second) - stat.mtime.should be_close(time, 1.second) + info = File.info(filename) + info.modification_time.should be_close(time, 1.second) ensure File.delete filename end @@ -1254,4 +1266,13 @@ describe "File" do File.match?("ab{{c,d}ef,}", "abdef").should be_true end end + + describe File::Permissions do + it "does to_s" do + perm = File::Permissions.flags(OwnerAll, GroupRead, GroupWrite, OtherRead) + perm.to_s.should eq("rwxrw-r-- (0o764)") + perm.inspect.should eq("rwxrw-r-- (0o764)") + perm.pretty_inspect.should eq("rwxrw-r-- (0o764)") + end + end end diff --git a/spec/std/file_utils_spec.cr b/spec/std/file_utils_spec.cr index dc1cbfb0c4df..487e1df835b1 100644 --- a/spec/std/file_utils_spec.cr +++ b/spec/std/file_utils_spec.cr @@ -136,6 +136,23 @@ describe "FileUtils" do end end + it "copies permissions" do + src_path = File.join(__DIR__, "data/new_test_file.txt") + out_path = File.join(__DIR__, "data/test_file_cp.txt") + begin + File.write(src_path, "foo") + File.chmod(src_path, 0o700) + + FileUtils.cp(src_path, out_path) + + File.info(out_path).permissions.should eq(File::Permissions.new(0o700)) + FileUtils.cmp(src_path, out_path).should be_true + ensure + File.delete(src_path) if File.exists?(out_path) + File.delete(out_path) if File.exists?(out_path) + end + end + it "raises an error if the directory doesn't exists" do expect_raises(ArgumentError, "No such directory : not_existing_dir") do FileUtils.cp({File.join(__DIR__, "data/test_file.text")}, "not_existing_dir") diff --git a/spec/std/http/server/handlers/static_file_handler_spec.cr b/spec/std/http/server/handlers/static_file_handler_spec.cr index d63629ba5236..553036034b2f 100644 --- a/spec/std/http/server/handlers/static_file_handler_spec.cr +++ b/spec/std/http/server/handlers/static_file_handler_spec.cr @@ -24,26 +24,26 @@ describe HTTP::StaticFileHandler do context "with header If-Modified-Since" do it "should return 304 Not Modified if file mtime is equal" do headers = HTTP::Headers.new - headers["If-Modified-Since"] = HTTP.rfc1123_date(File.stat("#{__DIR__}/static/test.txt").mtime) + headers["If-Modified-Since"] = HTTP.rfc1123_date(File.info("#{__DIR__}/static/test.txt").modification_time) response = handle HTTP::Request.new("GET", "/test.txt", headers), ignore_body: true response.status_code.should eq(304) - response.headers["Last-Modified"].should eq(HTTP.rfc1123_date(File.stat("#{__DIR__}/static/test.txt").mtime)) + response.headers["Last-Modified"].should eq(HTTP.rfc1123_date(File.info("#{__DIR__}/static/test.txt").modification_time)) end it "should return 304 Not Modified if file mtime is older" do headers = HTTP::Headers.new - headers["If-Modified-Since"] = HTTP.rfc1123_date(File.stat("#{__DIR__}/static/test.txt").mtime + 1.hour) + headers["If-Modified-Since"] = HTTP.rfc1123_date(File.info("#{__DIR__}/static/test.txt").modification_time + 1.hour) response = handle HTTP::Request.new("GET", "/test.txt", headers), ignore_body: true response.status_code.should eq(304) - response.headers["Last-Modified"].should eq(HTTP.rfc1123_date(File.stat("#{__DIR__}/static/test.txt").mtime)) + response.headers["Last-Modified"].should eq(HTTP.rfc1123_date(File.info("#{__DIR__}/static/test.txt").modification_time)) end it "should serve file if file mtime is younger" do headers = HTTP::Headers.new - headers["If-Modified-Since"] = HTTP.rfc1123_date(File.stat("#{__DIR__}/static/test.txt").mtime - 1.hour) + headers["If-Modified-Since"] = HTTP.rfc1123_date(File.info("#{__DIR__}/static/test.txt").modification_time - 1.hour) response = handle HTTP::Request.new("GET", "/test.txt") response.status_code.should eq(200) - response.headers["Last-Modified"].should eq(HTTP.rfc1123_date(File.stat("#{__DIR__}/static/test.txt").mtime)) + response.headers["Last-Modified"].should eq(HTTP.rfc1123_date(File.info("#{__DIR__}/static/test.txt").modification_time)) response.body.should eq(File.read("#{__DIR__}/static/test.txt")) end end diff --git a/src/compiler/crystal/codegen/cache_dir.cr b/src/compiler/crystal/codegen/cache_dir.cr index 225c6506f1f9..008edbb9faba 100644 --- a/src/compiler/crystal/codegen/cache_dir.cr +++ b/src/compiler/crystal/codegen/cache_dir.cr @@ -107,7 +107,7 @@ module Crystal private def cleanup_dirs(entries) entries .select { |dir| Dir.exists?(dir) } - .sort_by! { |dir| File.stat(dir).mtime rescue Time.epoch(0) } + .sort_by! { |dir| File.info?(dir).try(&.modification_time) || Time.epoch(0) } .reverse! .skip(10) .each { |name| `rm -rf "#{name}"` rescue nil } diff --git a/src/compiler/crystal/macros/macros.cr b/src/compiler/crystal/macros/macros.cr index efe34516c89c..783f197fc6e0 100644 --- a/src/compiler/crystal/macros/macros.cr +++ b/src/compiler/crystal/macros/macros.cr @@ -158,7 +158,7 @@ class Crystal::Program # Together with their timestamp # (this is the list of all effective files that were required) requires_with_timestamps = result.program.requires.map do |required_file| - epoch = File.stat(required_file).mtime.epoch + epoch = File.info(required_file).modification_time.epoch RequireWithTimestamp.new(required_file, epoch) end @@ -203,7 +203,7 @@ class Crystal::Program end new_requires_with_timestamps = required_files.map do |required_file| - epoch = File.stat(required_file).mtime.epoch + epoch = File.info(required_file).modification_time.epoch RequireWithTimestamp.new(required_file, epoch) end diff --git a/src/crystal/system/file_info.cr b/src/crystal/system/file_info.cr new file mode 100644 index 000000000000..fc7a5a71538d --- /dev/null +++ b/src/crystal/system/file_info.cr @@ -0,0 +1 @@ +require "./unix/file_info" diff --git a/src/crystal/system/unix/file.cr b/src/crystal/system/unix/file.cr index bf4cc31c67a6..b29796021d45 100644 --- a/src/crystal/system/unix/file.cr +++ b/src/crystal/system/unix/file.cr @@ -70,26 +70,23 @@ module Crystal::System::File tmpdir.rchop(::File::SEPARATOR) end - def self.stat?(path : String) : ::File::Stat? - if LibC.stat(path.check_no_null_byte, out stat) != 0 - if {Errno::ENOENT, Errno::ENOTDIR}.includes? Errno.value - return nil - else - raise Errno.new("Unable to get stat for '#{path}'") - end + def self.info?(path : String, follow_symlinks : Bool) : ::File::Info? + stat = uninitialized LibC::Stat + if follow_symlinks + ret = LibC.stat(path.check_no_null_byte, pointerof(stat)) + else + ret = LibC.lstat(path.check_no_null_byte, pointerof(stat)) end - ::File::Stat.new(stat) - end - def self.lstat?(path : String) : ::File::Stat? - if LibC.lstat(path.check_no_null_byte, out stat) != 0 + if ret == 0 + FileInfo.new(stat) + else if {Errno::ENOENT, Errno::ENOTDIR}.includes? Errno.value return nil else - raise Errno.new("Unable to get lstat for '#{path}'") + raise Errno.new("Unable to get info for '#{path}'") end end - ::File::Stat.new(stat) end def self.exists?(path) @@ -121,7 +118,7 @@ module Crystal::System::File raise Errno.new("Error changing owner of '#{path}'") if ret == -1 end - def self.chmod(path, mode : Int) + def self.chmod(path, mode) if LibC.chmod(path, mode) == -1 raise Errno.new("Error changing permissions of '#{path}'") end diff --git a/src/crystal/system/unix/file_descriptor.cr b/src/crystal/system/unix/file_descriptor.cr index d3762934e4e0..fb1848f946ca 100644 --- a/src/crystal/system/unix/file_descriptor.cr +++ b/src/crystal/system/unix/file_descriptor.cr @@ -58,11 +58,12 @@ module Crystal::System::FileDescriptor r end - private def system_stat + private def system_info if LibC.fstat(@fd, out stat) != 0 - raise Errno.new("Unable to get stat") + raise Errno.new("Unable to get info") end - ::File::Stat.new(stat) + + FileInfo.new(stat) end private def system_seek(offset, whence : IO::Seek) : Nil diff --git a/src/crystal/system/unix/file_info.cr b/src/crystal/system/unix/file_info.cr new file mode 100644 index 000000000000..94052f15c99e --- /dev/null +++ b/src/crystal/system/unix/file_info.cr @@ -0,0 +1,60 @@ +struct Crystal::System::FileInfo < ::File::Info + def initialize(@stat : LibC::Stat) + end + + def size : UInt64 + @stat.st_size.to_u64 + end + + def permissions : ::File::Permissions + ::File::Permissions.new((@stat.st_mode & 0o777).to_i16) + end + + def type : ::File::Type + case @stat.st_mode & LibC::S_IFMT + when LibC::S_IFBLK + ::File::Type::BlockDevice + when LibC::S_IFCHR + ::File::Type::CharacterDevice + when LibC::S_IFDIR + ::File::Type::Directory + when LibC::S_IFIFO + ::File::Type::Pipe + when LibC::S_IFLNK + ::File::Type::Symlink + when LibC::S_IFREG + ::File::Type::File + when LibC::S_IFSOCK + ::File::Type::Socket + else + ::File::Type::Unknown + end + end + + def flags : ::File::Flags + flags = ::File::Flags::None + flags |= ::File::Flags::SetUser if @stat.st_mode.bits_set? LibC::S_ISUID + flags |= ::File::Flags::SetGroup if @stat.st_mode.bits_set? LibC::S_ISGID + flags |= ::File::Flags::Sticky if @stat.st_mode.bits_set? LibC::S_ISVTX + end + + def modification_time : ::Time + {% if flag?(:darwin) %} + ::Time.new(@stat.st_mtimespec, ::Time::Location::UTC) + {% else %} + ::Time.new(@stat.st_mtim, ::Time::Location::UTC) + {% end %} + end + + def owner : UInt32 + @stat.st_uid.to_u32 + end + + def group : UInt32 + @stat.st_gid.to_u32 + end + + def ==(other : ::File::Info) : Bool + @stat.st_dev == other.@stat.st_dev && @stat.st_ino == other.@stat.st_ino + end +end diff --git a/src/crystal/system/unix/getrandom.cr b/src/crystal/system/unix/getrandom.cr index f56eb399d6e8..b5db87730f84 100644 --- a/src/crystal/system/unix/getrandom.cr +++ b/src/crystal/system/unix/getrandom.cr @@ -15,7 +15,7 @@ module Crystal::System::Random @@getrandom_available = true else urandom = ::File.open("/dev/urandom", "r") - return unless urandom.stat.chardev? + return unless urandom.info.type.character_device? urandom.close_on_exec = true urandom.sync = true # don't buffer bytes diff --git a/src/crystal/system/unix/urandom.cr b/src/crystal/system/unix/urandom.cr index deb037ee2bca..c168a07994d9 100644 --- a/src/crystal/system/unix/urandom.cr +++ b/src/crystal/system/unix/urandom.cr @@ -8,7 +8,7 @@ module Crystal::System::Random @@initialized = true urandom = ::File.open("/dev/urandom", "r") - return unless urandom.stat.chardev? + return unless urandom.info.type.character_device? urandom.close_on_exec = true urandom.sync = true # don't buffer bytes diff --git a/src/dir.cr b/src/dir.cr index b7751d69191b..80287a88190c 100644 --- a/src/dir.cr +++ b/src/dir.cr @@ -193,8 +193,8 @@ class Dir # Returns `true` if the given path exists and is a directory def self.exists?(path) : Bool - if stat = File.stat?(path) - stat.directory? + if info = File.info?(path) + info.type.directory? else false end diff --git a/src/dir/glob.cr b/src/dir/glob.cr index ddda136bd7f4..61772ffb9973 100644 --- a/src/dir/glob.cr +++ b/src/dir/glob.cr @@ -267,11 +267,11 @@ class Dir end private def self.dir?(path) - return true unless path - stat = File.lstat(path) - stat.directory? && !stat.symlink? - rescue Errno - false + if info = File.info?(path, follow_symlinks: false) + info.type.directory? + else + false + end end private def self.join(path, entry) diff --git a/src/file.cr b/src/file.cr index ce6a4a1d06b5..632b8a01e022 100644 --- a/src/file.cr +++ b/src/file.cr @@ -16,7 +16,7 @@ class File < IO::FileDescriptor {% end %} # :nodoc: - DEFAULT_CREATE_MODE = LibC::S_IRUSR | LibC::S_IWUSR | LibC::S_IRGRP | LibC::S_IROTH + DEFAULT_CREATE_PERMISSIONS = File::Permissions.new(0o644) include Crystal::System::File @@ -27,67 +27,47 @@ class File < IO::FileDescriptor super(fd, blocking) end - def self.new(filename : String, mode = "r", perm = DEFAULT_CREATE_MODE, encoding = nil, invalid = nil) + def self.new(filename : String, mode = "r", perm = DEFAULT_CREATE_PERMISSIONS, encoding = nil, invalid = nil) fd = Crystal::System::File.open(filename, mode, perm) new(filename, fd, blocking: true, encoding: encoding, invalid: invalid) end getter path : String - # Returns a `File::Stat` object for the file given by *path* or returns `nil` - # if the file does not exist. Raises `Errno` in case of an error. In case of - # a symbolic link it is followed and information about the target is returned. + # Returns a `File::Info` object for the file given by *path* or returns `nil` + # if the file does not exist. # - # ``` - # File.write("foo", "foo") - # File.stat?("foo").try(&.size) # => 3 - # File.stat?("non_existent") # => nil - # ``` - def self.stat?(path : String) : Stat? - Crystal::System::File.stat?(path) - end - - # Returns a `File::Stat` object for the file given by *path* or returns `nil` - # if the file does not exist. Raises `Errno` in case of an error. In case of - # a symbolic link information about the link itself is returned. + # If *follow_symlinks* is set (the default), symbolic links are followed. Otherwise, + # symbolic links return information on the symlink itself. # # ``` # File.write("foo", "foo") - # File.lstat?("foo").try(&.size) # => 3 + # File.info?("foo").try(&.size) # => 3 + # File.info?("non_existent") # => nil # # File.symlink("foo", "bar") - # File.lstat?("bar").try(&.symlink?) # => true - # - # File.lstat?("non_existent") # => nil + # File.info?("bar", follow_symlinks: false).try(&.type.symlink?) # => true # ``` - def self.lstat?(path : String) : Stat? - Crystal::System::File.lstat?(path) + def self.info?(path : String, follow_symlinks = true) : Info? + Crystal::System::File.info?(path, follow_symlinks) end - # Returns a `File::Stat` object for the file given by *path* or raises - # `Errno` in case of an error. In case of a symbolic link - # it is followed and information about the target is returned. + # Returns a `File::Info` object for the file given by *path* or raises + # `Errno` in case of an error. # - # ``` - # File.write("foo", "foo") - # File.stat("foo").size # => 3 - # File.stat("foo").mtime # => 2015-09-23 06:24:19 UTC - # ``` - def self.stat(path) : Stat - stat?(path) || raise Errno.new("Unable to get stat for #{path.inspect}") - end - - # Returns a `File::Stat` object for the file given by *path* or raises - # `Errno` in case of an error. In case of a symbolic link - # information about the link itself is returned. + # If *follow_symlinks* is set (the default), symbolic links are followed. Otherwise, + # symbolic links return information on the symlink itself. # # ``` # File.write("foo", "foo") - # File.lstat("foo").size # => 3 - # File.lstat("foo").mtime # => 2015-09-23 06:24:19 UTC + # File.info("foo").size # => 3 + # File.info("foo").modification_time # => 2015-09-23 06:24:19 UTC + # + # File.symlink("foo", "bar") + # File.info("bar", follow_symlinks: false).type.symlink? # => true # ``` - def self.lstat(path) : Stat - lstat?(path) || raise Errno.new("Unable to get stat for #{path.inspect}") + def self.info(path, follow_symlinks = true) : Info + info?(path, follow_symlinks) || raise Errno.new("Unable to get info for #{path.inspect}") end # Returns `true` if *path* exists else returns `false` @@ -111,7 +91,7 @@ class File < IO::FileDescriptor # File.size("foo") # => 3 # ``` def self.size(filename) : UInt64 - stat(filename).size + info(filename).size rescue ex : Errno raise Errno.new("Error determining size of #{filename.inspect}", ex.errno) end @@ -169,8 +149,8 @@ class File < IO::FileDescriptor # File.file?("foobar") # => false # ``` def self.file?(path) : Bool - if stat = stat?(path) - stat.file? + if info = info?(path) + info.type.file? else false end @@ -268,13 +248,13 @@ class File < IO::FileDescriptor # # ``` # File.chmod("foo", 0o755) - # File.stat("foo").perm # => 0o755 + # File.info("foo").permissions # => 0o755 # # File.chmod("foo", 0o700) - # File.stat("foo").perm # => 0o700 + # File.info("foo").permissions # => 0o700 # ``` - def self.chmod(path, mode : Int) - Crystal::System::File.chmod(path, mode) + def self.chmod(path, permissions : Int | Permissions) + Crystal::System::File.chmod(path, permissions) end # Delete the file at *path*. Deleting non-existent file will raise an exception. @@ -575,8 +555,8 @@ class File < IO::FileDescriptor # Returns `true` if the *path* is a symbolic link. def self.symlink?(path) : Bool - if stat = lstat?(path) - stat.symlink? + if info = info?(path, follow_symlinks: false) + info.type.symlink? else false end @@ -584,14 +564,14 @@ class File < IO::FileDescriptor # Opens the file named by *filename*. If a file is being created, its initial # permissions may be set using the *perm* parameter. - def self.open(filename, mode = "r", perm = DEFAULT_CREATE_MODE, encoding = nil, invalid = nil) : self + def self.open(filename, mode = "r", perm = DEFAULT_CREATE_PERMISSIONS, encoding = nil, invalid = nil) : self new filename, mode, perm, encoding, invalid end # Opens the file named by *filename*. If a file is being created, its initial # permissions may be set using the *perm* parameter. Then given block will be passed the opened # file as an argument, the file will be automatically closed when the block returns. - def self.open(filename, mode = "r", perm = DEFAULT_CREATE_MODE, encoding = nil, invalid = nil) + def self.open(filename, mode = "r", perm = DEFAULT_CREATE_PERMISSIONS, encoding = nil, invalid = nil) file = new filename, mode, perm, encoding, invalid begin yield file @@ -679,7 +659,7 @@ class File < IO::FileDescriptor # If it's an `IO`, all bytes from the `IO` will be written. # Otherwise, the string representation of *content* will be written # (the result of invoking `to_s` on *content*). - def self.write(filename, content, perm = DEFAULT_CREATE_MODE, encoding = nil, invalid = nil, mode = "w") + def self.write(filename, content, perm = DEFAULT_CREATE_PERMISSIONS, encoding = nil, invalid = nil, mode = "w") open(filename, mode, perm, encoding: encoding, invalid: invalid) do |file| case content when Bytes @@ -768,7 +748,7 @@ class File < IO::FileDescriptor # Return the size in bytes of the currently opened file. def size - stat.size + info.size end # Truncates the file to the specified *size*. Requires that the current file is opened diff --git a/src/file/info.cr b/src/file/info.cr new file mode 100644 index 000000000000..0f323f8292af --- /dev/null +++ b/src/file/info.cr @@ -0,0 +1,128 @@ +class File + # Represents the various behaviour-altering flags which can be set on files. + # Not all flags will be supported on all platforms. + @[Flags] + enum Flags : UInt8 + SetUser + SetGroup + Sticky + end + + # Represents the type of a file. Not all types will be supported on all + # platforms. + enum Type : UInt8 + File + Directory + Symlink + Socket + Pipe + CharacterDevice + BlockDevice + Unknown + + # Returns true if `self` is a `CharacterDevice` or a `BlockDevice`. + def device? + character_device? || block_device? + end + end + + # Represents a set of access permissions for a file. Not all permission sets + # will be supported on all platforms. + # + # The binary representation of this enum is defined to be same representation + # as the permission bits of a unix `st_mode` field. `File::Permissions` + # can also be compared to it's underlying bitset, for example + # `File::Permissions::All == 0o777` will always be `true`. + # + # On windows, only the `OwnerWrite` bit is effective. All file permissions + # will either be `0o444` for read-only files or `0o666` for read-write files. + # Directories are always mode `0o555` for read-only or `0o777`. + @[Flags] + enum Permissions : Int16 + OtherExecute = 0o001 + OtherWrite = 0o002 + OtherRead = 0o004 + OtherAll = 0o007 + + GroupExecute = 0o010 + GroupWrite = 0o020 + GroupRead = 0o040 + GroupAll = 0o070 + + OwnerExecute = 0o100 + OwnerWrite = 0o200 + OwnerRead = 0o400 + OwnerAll = 0o700 + + def self.new(int : Int) + new(int.to_i16) + end + + def to_s(io) + io << (owner_read? ? 'r' : '-') + io << (owner_write? ? 'w' : '-') + io << (owner_execute? ? 'x' : '-') + + io << (group_read? ? 'r' : '-') + io << (group_write? ? 'w' : '-') + io << (group_execute? ? 'x' : '-') + + io << (other_read? ? 'r' : '-') + io << (other_write? ? 'w' : '-') + io << (other_execute? ? 'x' : '-') + + io << " (0o" << self.to_i.to_s(8) << ')' + end + end + + # A `File::Info` contains metadata regarding a file. It is returned by + # `File.info`, and `File#info`. + abstract struct Info + # Size of the file, in bytes. + abstract def size : UInt64 + + # The permissions of the file. + abstract def permissions : Permissions + + # The type of the file. + abstract def type : Type + + # The special flags this file has set. + abstract def flags : Flags + + # The last time this file was modified. + abstract def modification_time : Time + + # The user ID of the file's owner. + abstract def owner : UInt32 + + # The group ID that the file belongs to. + abstract def group : UInt32 + + # Two `File::Info`s are equal if and only if they are of the same file. + # + # On unix, this compares device and inode fields, and will compare equal for + # hard linked files. + abstract def ==(other : File::Info) + + # Returns true if this `Info` represents a standard file. Shortcut for + # `type.file?`. + def file? + type.file? + end + + # Returns true if this `Info` represents a directory. Shortcut for + # `type.directory?`. + def directory? + type.directory? + end + + # Returns true if this `Info` represents a symbolic link to another file. + # Shortcut for `type.symlink?`. + def symlink? + type.symlink? + end + end +end + +require "crystal/system/file_info" diff --git a/src/file/stat.cr b/src/file/stat.cr deleted file mode 100644 index 22045ccd10b5..000000000000 --- a/src/file/stat.cr +++ /dev/null @@ -1,209 +0,0 @@ -require "c/sys/stat" - -class File - struct Stat - def self.new(filename : String) - File.stat(filename) - end - - {% if flag?(:win32) %} - # :nodoc: - def initialize(@stat : LibC::Stat64) - end - {% else %} - # :nodoc: - def initialize(@stat : LibC::Stat) - end - {% end %} - - def atime - {% if flag?(:darwin) %} - time @stat.st_atimespec - {% elsif flag?(:win32) %} - time @stat.st_atime - {% else %} - time @stat.st_atim - {% end %} - end - - def blksize - {% if flag?(:win32) %} - raise NotImplementedError.new("File::Stat#blksize") - {% else %} - @stat.st_blksize - {% end %} - end - - def blocks - {% if flag?(:win32) %} - raise NotImplementedError.new("File::Stat#blocks") - {% else %} - @stat.st_blocks - {% end %} - end - - def ctime - {% if flag?(:darwin) %} - time @stat.st_ctimespec - {% elsif flag?(:win32) %} - time @stat.st_ctime - {% else %} - time @stat.st_ctim - {% end %} - end - - def dev - @stat.st_dev - end - - def gid - @stat.st_gid - end - - def ino - @stat.st_ino - end - - def mode - @stat.st_mode - end - - # permission bits of mode - def perm - mode & 0o7777 - end - - def mtime - {% if flag?(:darwin) %} - time @stat.st_mtimespec - {% elsif flag?(:win32) %} - time @stat.st_mtime - {% else %} - time @stat.st_mtim - {% end %} - end - - def nlink - @stat.st_nlink - end - - def rdev - @stat.st_rdev - end - - def size - @stat.st_size.to_u64 - end - - def uid - @stat.st_uid - end - - def inspect(io) - io << "#", left_break: " ", right_break: nil) do - pp.text "dev=0x#{dev.to_s(16)}" - pp.comma - pp.text "ino=#{ino}" - pp.comma - pp.text "mode=0o#{mode.to_s(8)}" - pp.comma - pp.text "nlink=#{nlink}" - pp.comma - pp.text "uid=#{uid}" - pp.comma - pp.text "gid=#{gid}" - pp.comma - pp.text "rdev=0x#{rdev.to_s(16)}" - pp.comma - pp.text "size=#{size}" - pp.comma - {% unless flag?(:win32) %} - # These two getters raise NotImplementedError on windows. - pp.text "blksize=#{blksize}" - pp.comma - pp.text "blocks=#{blocks}" - pp.comma - {% end %} - pp.text "atime=#{atime}" - pp.comma - pp.text "mtime=#{mtime}" - pp.comma - pp.text "ctime=#{ctime}" - end - end - - def blockdev? - (@stat.st_mode & LibC::S_IFMT) == LibC::S_IFBLK - end - - def chardev? - (@stat.st_mode & LibC::S_IFMT) == LibC::S_IFCHR - end - - def directory? - (@stat.st_mode & LibC::S_IFMT) == LibC::S_IFDIR - end - - def file? - (@stat.st_mode & LibC::S_IFMT) == LibC::S_IFREG - end - - def pipe? - (@stat.st_mode & LibC::S_IFMT) == LibC::S_IFIFO - end - - def setuid? - (@stat.st_mode & LibC::S_IFMT) == LibC::S_ISUID - end - - def setgid? - (@stat.st_mode & LibC::S_IFMT) == LibC::S_ISGID - end - - def symlink? - (@stat.st_mode & LibC::S_IFMT) == LibC::S_IFLNK - end - - def socket? - (@stat.st_mode & LibC::S_IFMT) == LibC::S_IFSOCK - end - - def sticky? - (@stat.st_mode & LibC::S_IFMT) == LibC::S_ISVTX - end - - {% if flag?(:win32) %} - private def time(value) - Time.epoch(value) - end - {% else %} - private def time(value) - Time.new value, Time::Location::UTC - end - {% end %} - end -end diff --git a/src/file_utils.cr b/src/file_utils.cr index f9e34450384c..ea5d1e6990d1 100644 --- a/src/file_utils.cr +++ b/src/file_utils.cr @@ -102,12 +102,12 @@ module FileUtils # ``` # File.chmod("afile", 0o600) # FileUtils.cp("afile", "afile_copy") - # File.stat("afile_copy").perm # => 0o600 + # File.info("afile_copy").permissions # => 0o600 # ``` def cp(src_path : String, dest : String) File.open(src_path) do |s| dest += File::SEPARATOR + File.basename(src_path) if Dir.exists?(dest) - File.open(dest, "wb", s.stat.mode) do |d| + File.open(dest, "wb", s.info.permissions) do |d| IO.copy(s, d) end end diff --git a/src/http/server/handlers/static_file_handler.cr b/src/http/server/handlers/static_file_handler.cr index 7aa49fb1ac5b..2ff731d8056a 100644 --- a/src/http/server/handlers/static_file_handler.cr +++ b/src/http/server/handlers/static_file_handler.cr @@ -64,7 +64,7 @@ class HTTP::StaticFileHandler context.response.content_type = "text/html" directory_listing(context.response, request_path, file_path) elsif is_file - last_modified = File.stat(file_path).mtime + last_modified = File.info(file_path).modification_time context.response.headers["Last-Modified"] = HTTP.rfc1123_date(last_modified) if if_modified_since = context.request.headers["If-Modified-Since"]? diff --git a/src/io/file_descriptor.cr b/src/io/file_descriptor.cr index 11d07a88427a..c265287c94bc 100644 --- a/src/io/file_descriptor.cr +++ b/src/io/file_descriptor.cr @@ -44,8 +44,8 @@ class IO::FileDescriptor < IO end {% end %} - def stat - system_stat + def info + system_info end # Seeks to a given *offset* (in bytes) according to the *whence* argument. diff --git a/src/tempfile.cr b/src/tempfile.cr index c683a50f236a..55d704cc075b 100644 --- a/src/tempfile.cr +++ b/src/tempfile.cr @@ -11,10 +11,10 @@ require "c/stdlib" # file.print("foobar") # end # -# File.size(tempfile.path) # => 6 -# File.stat(tempfile.path).mtime # => 2015-10-20 13:11:12 UTC -# File.exists?(tempfile.path) # => true -# File.read_lines(tempfile.path) # => ["foobar"] +# File.size(tempfile.path) # => 6 +# File.info(tempfile.path).modification_time # => 2015-10-20 13:11:12 UTC +# File.exists?(tempfile.path) # => true +# File.read_lines(tempfile.path) # => ["foobar"] # ``` # # Files created from this class are stored in a directory that handles diff --git a/src/time/location/loader.cr b/src/time/location/loader.cr index 4ebb0e8048f2..57b2eab5d083 100644 --- a/src/time/location/loader.cr +++ b/src/time/location/loader.cr @@ -44,7 +44,7 @@ class Time::Location private def self.open_file_cached(name : String, path : String) return nil unless File.exists?(path) - mtime = File.stat(path).mtime + mtime = File.info(path).modification_time if (cache = @@location_cache[name]?) && cache[:time] == mtime return cache[:location] else