Skip to content

Commit 48a1130

Browse files
authoredJan 13, 2018
Simplify Crystal::System interface by adding File.stat? and lstat? (#5553)
By providing these methods we can make the implementation of File.empty? and File.file? platform-unspecific. This makes the interface to Crystal::System::File smaller and cleaner.
1 parent 77de91f commit 48a1130

File tree

7 files changed

+160
-64
lines changed

7 files changed

+160
-64
lines changed
 

‎spec/std/dir_spec.cr

+8-1
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,17 @@ describe "Dir" do
3737
end
3838

3939
it "tests empty? on nonexistent directory" do
40-
expect_raises Errno do
40+
expect_raises(Errno, /Error determining size of/) do
4141
Dir.empty?(File.join([__DIR__, "/foo/bar/"]))
4242
end
4343
end
44+
45+
it "tests empty? on a directory path to a file" do
46+
ex = expect_raises(Errno, /Error determining size of/) do
47+
Dir.empty?("#{__FILE__}/")
48+
end
49+
ex.errno.should eq(Errno::ENOTDIR)
50+
end
4451
end
4552

4653
it "tests mkdir and rmdir with a new path" do

‎spec/std/file_spec.cr

+76-1
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,18 @@ describe "File" do
116116

117117
it "raises an error when the file does not exist" do
118118
filename = "#{__DIR__}/data/non_existing_file.txt"
119-
expect_raises Errno do
119+
expect_raises(Errno, /Error determining size/) do
120120
File.empty?(filename)
121121
end
122122
end
123+
124+
it "raises an error when a component of the path is a file" do
125+
filename = "#{__DIR__}/data/non_existing_file.txt"
126+
ex = expect_raises(Errno, /Error determining size/) do
127+
File.empty?("#{__FILE__}/")
128+
end
129+
ex.errno.should eq(Errno::ENOTDIR)
130+
end
123131
end
124132

125133
describe "exists?" do
@@ -130,24 +138,52 @@ describe "File" do
130138
it "gives false" do
131139
File.exists?("#{__DIR__}/data/non_existing_file.txt").should be_false
132140
end
141+
142+
it "gives false when a component of the path is a file" do
143+
File.exists?("#{__FILE__}/").should be_false
144+
end
133145
end
134146

135147
describe "executable?" do
136148
it "gives false" do
137149
File.executable?("#{__DIR__}/data/test_file.txt").should be_false
138150
end
151+
152+
it "gives false when the file doesn't exist" do
153+
File.executable?("#{__DIR__}/data/non_existing_file.txt").should be_false
154+
end
155+
156+
it "gives false when a component of the path is a file" do
157+
File.executable?("#{__FILE__}/").should be_false
158+
end
139159
end
140160

141161
describe "readable?" do
142162
it "gives true" do
143163
File.readable?("#{__DIR__}/data/test_file.txt").should be_true
144164
end
165+
166+
it "gives false when the file doesn't exist" do
167+
File.readable?("#{__DIR__}/data/non_existing_file.txt").should be_false
168+
end
169+
170+
it "gives false when a component of the path is a file" do
171+
File.readable?("#{__FILE__}/").should be_false
172+
end
145173
end
146174

147175
describe "writable?" do
148176
it "gives true" do
149177
File.writable?("#{__DIR__}/data/test_file.txt").should be_true
150178
end
179+
180+
it "gives false when the file doesn't exist" do
181+
File.writable?("#{__DIR__}/data/non_existing_file.txt").should be_false
182+
end
183+
184+
it "gives false when a component of the path is a file" do
185+
File.writable?("#{__FILE__}/").should be_false
186+
end
151187
end
152188

153189
describe "file?" do
@@ -158,6 +194,14 @@ describe "File" do
158194
it "gives false" do
159195
File.file?("#{__DIR__}/data").should be_false
160196
end
197+
198+
it "gives false when the file doesn't exist" do
199+
File.file?("#{__DIR__}/data/non_existing_file.txt").should be_false
200+
end
201+
202+
it "gives false when a component of the path is a file" do
203+
File.file?("#{__FILE__}/").should be_false
204+
end
161205
end
162206

163207
describe "directory?" do
@@ -168,6 +212,14 @@ describe "File" do
168212
it "gives false" do
169213
File.directory?("#{__DIR__}/data/test_file.txt").should be_false
170214
end
215+
216+
it "gives false when the directory doesn't exist" do
217+
File.directory?("#{__DIR__}/data/non_existing").should be_false
218+
end
219+
220+
it "gives false when a component of the path is a file" do
221+
File.directory?("#{__FILE__}/").should be_false
222+
end
171223
end
172224

173225
describe "link" do
@@ -204,6 +256,14 @@ describe "File" do
204256
File.symlink?("#{__DIR__}/data/test_file.txt").should be_false
205257
File.symlink?("#{__DIR__}/data/unknown_file.txt").should be_false
206258
end
259+
260+
it "gives false when the symlink doesn't exist" do
261+
File.symlink?("#{__DIR__}/data/non_existing_file.txt").should be_false
262+
end
263+
264+
it "gives false when a component of the path is a file" do
265+
File.symlink?("#{__FILE__}/").should be_false
266+
end
207267
end
208268

209269
it "gets dirname" do
@@ -376,6 +436,21 @@ describe "File" do
376436
file.size.should eq(240)
377437
end
378438
end
439+
440+
it "raises an error when the file does not exist" do
441+
filename = "#{__DIR__}/data/non_existing_file.txt"
442+
expect_raises(Errno, /Error determining size/) do
443+
File.size(filename)
444+
end
445+
end
446+
447+
it "raises an error when a component of the path is a file" do
448+
filename = "#{__DIR__}/data/non_existing_file.txt"
449+
ex = expect_raises(Errno, /Error determining size/) do
450+
File.size("#{__FILE__}/")
451+
end
452+
ex.errno.should eq(Errno::ENOTDIR)
453+
end
379454
end
380455

381456
describe "delete" do

‎src/crystal/system/dir.cr

-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ module Crystal::System::Dir
1919
# Sets the current working directory of the application.
2020
# def self.current=(path : String)
2121

22-
# Returns `true` if *path* exists and is a directory.
23-
# def self.exists?(path : String) : Bool
24-
2522
# Creates a new directory at *path*. The UNIX-style directory mode *node*
2623
# must be applied.
2724
# def self.create(path : String, mode : Int32) : Nil

‎src/crystal/system/unix/dir.cr

-12
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,6 @@ module Crystal::System::Dir
4848
path
4949
end
5050

51-
def self.exists?(path : String) : Bool
52-
if LibC.stat(path.check_no_null_byte, out stat) != 0
53-
if Errno.value == Errno::ENOENT || Errno.value == Errno::ENOTDIR
54-
return false
55-
else
56-
raise Errno.new("stat")
57-
end
58-
end
59-
60-
(stat.st_mode & LibC::S_IFMT) == LibC::S_IFDIR
61-
end
62-
6351
def self.create(path : String, mode : Int32) : Nil
6452
if LibC.mkdir(path.check_no_null_byte, mode) == -1
6553
raise Errno.new("Unable to create directory '#{path}'")

‎src/crystal/system/unix/file.cr

+13-35
Original file line numberDiff line numberDiff line change
@@ -70,28 +70,28 @@ module Crystal::System::File
7070
tmpdir.rchop(::File::SEPARATOR)
7171
end
7272

73-
def self.stat(path)
73+
def self.stat?(path : String) : ::File::Stat?
7474
if LibC.stat(path.check_no_null_byte, out stat) != 0
75-
raise Errno.new("Unable to get stat for '#{path}'")
75+
if {Errno::ENOENT, Errno::ENOTDIR}.includes? Errno.value
76+
return nil
77+
else
78+
raise Errno.new("Unable to get stat for '#{path}'")
79+
end
7680
end
7781
::File::Stat.new(stat)
7882
end
7983

80-
def self.lstat(path)
84+
def self.lstat?(path : String) : ::File::Stat?
8185
if LibC.lstat(path.check_no_null_byte, out stat) != 0
82-
raise Errno.new("Unable to get lstat for '#{path}'")
86+
if {Errno::ENOENT, Errno::ENOTDIR}.includes? Errno.value
87+
return nil
88+
else
89+
raise Errno.new("Unable to get lstat for '#{path}'")
90+
end
8391
end
8492
::File::Stat.new(stat)
8593
end
8694

87-
def self.empty?(path)
88-
begin
89-
stat(path).size == 0
90-
rescue Errno
91-
raise Errno.new("Error determining size of '#{path}'")
92-
end
93-
end
94-
9595
def self.exists?(path)
9696
accessible?(path, LibC::F_OK)
9797
end
@@ -112,19 +112,8 @@ module Crystal::System::File
112112
LibC.access(path.check_no_null_byte, flag) == 0
113113
end
114114

115-
def self.file?(path) : Bool
116-
if LibC.stat(path.check_no_null_byte, out stat) != 0
117-
if Errno.value == Errno::ENOENT
118-
return false
119-
else
120-
raise Errno.new("stat")
121-
end
122-
end
123-
::File::Stat.new(stat).file?
124-
end
125-
126115
def self.chown(path, uid : Int, gid : Int, follow_symlinks)
127-
ret = if !follow_symlinks && symlink?(path)
116+
ret = if !follow_symlinks && ::File.symlink?(path)
128117
LibC.lchown(path, uid, gid)
129118
else
130119
LibC.chown(path, uid, gid)
@@ -163,17 +152,6 @@ module Crystal::System::File
163152
ret
164153
end
165154

166-
def self.symlink?(path)
167-
if LibC.lstat(path.check_no_null_byte, out stat) != 0
168-
if Errno.value == Errno::ENOENT
169-
return false
170-
else
171-
raise Errno.new("stat")
172-
end
173-
end
174-
(stat.st_mode & LibC::S_IFMT) == LibC::S_IFLNK
175-
end
176-
177155
def self.rename(old_filename, new_filename)
178156
code = LibC.rename(old_filename.check_no_null_byte, new_filename.check_no_null_byte)
179157
if code != 0

‎src/dir.cr

+5-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,11 @@ class Dir
193193

194194
# Returns `true` if the given path exists and is a directory
195195
def self.exists?(path) : Bool
196-
Crystal::System::Dir.exists? path
196+
if stat = File.stat?(path)
197+
stat.directory?
198+
else
199+
false
200+
end
197201
end
198202

199203
# Returns `true` if the directory at *path* is empty, otherwise returns `false`.

‎src/file.cr

+58-11
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,36 @@ class File < IO::FileDescriptor
3434

3535
getter path : String
3636

37+
# Returns a `File::Stat` object for the file given by *path* or returns `nil`
38+
# if the file does not exist. Raises `Errno` in case of an error. In case of
39+
# a symbolic link it is followed and information about the target is returned.
40+
#
41+
# ```
42+
# File.write("foo", "foo")
43+
# File.stat?("foo").try(&.size) # => 3
44+
# File.stat?("non_existent") # => nil
45+
# ```
46+
def self.stat?(path : String) : Stat?
47+
Crystal::System::File.stat?(path)
48+
end
49+
50+
# Returns a `File::Stat` object for the file given by *path* or returns `nil`
51+
# if the file does not exist. Raises `Errno` in case of an error. In case of
52+
# a symbolic link information about the link itself is returned.
53+
#
54+
# ```
55+
# File.write("foo", "foo")
56+
# File.lstat?("foo").try(&.size) # => 3
57+
#
58+
# File.symlink("foo", "bar")
59+
# File.lstat?("bar").try(&.symlink?) # => true
60+
#
61+
# File.lstat?("non_existent") # => nil
62+
# ```
63+
def self.lstat?(path : String) : Stat?
64+
Crystal::System::File.lstat?(path)
65+
end
66+
3767
# Returns a `File::Stat` object for the file given by *path* or raises
3868
# `Errno` in case of an error. In case of a symbolic link
3969
# it is followed and information about the target is returned.
@@ -44,20 +74,20 @@ class File < IO::FileDescriptor
4474
# File.stat("foo").mtime # => 2015-09-23 06:24:19 UTC
4575
# ```
4676
def self.stat(path) : Stat
47-
Crystal::System::File.stat(path)
77+
stat?(path) || raise Errno.new("Unable to get stat for #{path.inspect}")
4878
end
4979

5080
# Returns a `File::Stat` object for the file given by *path* or raises
5181
# `Errno` in case of an error. In case of a symbolic link
52-
# information about it is returned.
82+
# information about the link itself is returned.
5383
#
5484
# ```
5585
# File.write("foo", "foo")
5686
# File.lstat("foo").size # => 3
5787
# File.lstat("foo").mtime # => 2015-09-23 06:24:19 UTC
5888
# ```
5989
def self.lstat(path) : Stat
60-
Crystal::System::File.lstat(path)
90+
lstat?(path) || raise Errno.new("Unable to get stat for #{path.inspect}")
6191
end
6292

6393
# Returns `true` if *path* exists else returns `false`
@@ -72,6 +102,20 @@ class File < IO::FileDescriptor
72102
Crystal::System::File.exists?(path)
73103
end
74104

105+
# Returns the size of *filename* bytes. Raises `Errno` if the file at *path*
106+
# does not exist.
107+
#
108+
# ```
109+
# File.size("foo") # raises Errno
110+
# File.write("foo", "foo")
111+
# File.size("foo") # => 3
112+
# ```
113+
def self.size(filename) : UInt64
114+
stat(filename).size
115+
rescue ex : Errno
116+
raise Errno.new("Error determining size of #{filename.inspect}", ex.errno)
117+
end
118+
75119
# Returns `true` if the file at *path* is empty, otherwise returns `false`.
76120
# Raises `Errno` if the file at *path* does not exist.
77121
#
@@ -82,7 +126,7 @@ class File < IO::FileDescriptor
82126
# File.empty?("foo") # => false
83127
# ```
84128
def self.empty?(path) : Bool
85-
Crystal::System::File.empty?(path)
129+
size(path) == 0
86130
end
87131

88132
# Returns `true` if *path* is readable by the real user id of this process else returns `false`.
@@ -125,7 +169,11 @@ class File < IO::FileDescriptor
125169
# File.file?("foobar") # => false
126170
# ```
127171
def self.file?(path) : Bool
128-
Crystal::System::File.file?(path)
172+
if stat = stat?(path)
173+
stat.file?
174+
else
175+
false
176+
end
129177
end
130178

131179
# Returns `true` if the given *path* exists and is a directory.
@@ -527,7 +575,11 @@ class File < IO::FileDescriptor
527575

528576
# Returns `true` if the *path* is a symbolic link.
529577
def self.symlink?(path) : Bool
530-
Crystal::System::File.symlink?(path)
578+
if stat = lstat?(path)
579+
stat.symlink?
580+
else
581+
false
582+
end
531583
end
532584

533585
# Opens the file named by *filename*. If a file is being created, its initial
@@ -677,11 +729,6 @@ class File < IO::FileDescriptor
677729
end
678730
end
679731

680-
# Returns the size of *filename* bytes.
681-
def self.size(filename) : UInt64
682-
stat(filename.check_no_null_byte).size
683-
end
684-
685732
# Moves *old_filename* to *new_filename*.
686733
#
687734
# ```

0 commit comments

Comments
 (0)
Please sign in to comment.