dry-files 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class Files
5
+ # @since 0.1.0
6
+ # @api private
7
+ class Adapter
8
+ # @since 0.1.0
9
+ # @api private
10
+ def self.call(memory:)
11
+ if memory
12
+ require_relative "./memory_file_system"
13
+ MemoryFileSystem.new
14
+ else
15
+ require_relative "./file_system"
16
+ FileSystem.new
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class Files
5
+ # Dry::Files base error
6
+ #
7
+ # @since 0.1.0
8
+ # @api public
9
+ class Error < StandardError
10
+ end
11
+
12
+ # Wraps low level I/O errors
13
+ #
14
+ # @see https://ruby-doc.org/core/Errno.html
15
+ #
16
+ # @since 0.1.0
17
+ # @api public
18
+ class IOError < Error
19
+ # Instantiate a new `Dry::Files::IOError`
20
+ #
21
+ # @param cause [Exception] the low level exception
22
+ #
23
+ # @return [Dry::Files::IOError] the new error
24
+ #
25
+ # @since 0.1.0
26
+ # @api private
27
+ def initialize(cause)
28
+ super(cause.message)
29
+ @_cause = cause
30
+ end
31
+
32
+ # The original exception
33
+ #
34
+ # @see https://ruby-doc.org/core/Exception.html#method-i-cause
35
+ #
36
+ # @since 0.1.0
37
+ # @api public
38
+ def cause
39
+ @_cause
40
+ end
41
+ end
42
+
43
+ # File manipulations error.
44
+ # Raised when the given `target` cannot be found in `path`.
45
+ #
46
+ # @since 0.1.0
47
+ # @api public
48
+ class MissingTargetError < Error
49
+ # @param target [String,Regexp] the missing target
50
+ # @param path [String,Pathname] the file
51
+ #
52
+ # @return [Dry::Files::MissingTargetError] the new error
53
+ #
54
+ # @since 0.1.0
55
+ # @api private
56
+ def initialize(target, path)
57
+ super("cannot find `#{target}' in `#{path}'")
58
+
59
+ @_target = target
60
+ @_path = path
61
+ end
62
+
63
+ # The missing target
64
+ #
65
+ # @return [String, Regexp] the missing target
66
+ #
67
+ # @since 0.1.0
68
+ # @api public
69
+ def target
70
+ @_target
71
+ end
72
+
73
+ # The missing target
74
+ #
75
+ # @return [String,Pathname] the file
76
+ #
77
+ # @since 0.1.0
78
+ # @api public
79
+ def path
80
+ @_path
81
+ end
82
+ end
83
+
84
+ # Unknown memory node
85
+ #
86
+ # Raised by the memory adapter (used for testing purposes)
87
+ #
88
+ # @since 0.1.0
89
+ # @api public
90
+ class UnknownMemoryNodeError < Error
91
+ # Instantiate a new error
92
+ #
93
+ # @param node [String] node name
94
+ #
95
+ # @since 0.1.0
96
+ # @api public
97
+ def initialize(node)
98
+ super("unknown memory node `#{node}'")
99
+ end
100
+ end
101
+
102
+ # Not a memory file
103
+ #
104
+ # Raised by the memory adapter (used for testing purposes)
105
+ #
106
+ # @since 0.1.0
107
+ # @api public
108
+ class NotMemoryFileError < Error
109
+ # Instantiate a new error
110
+ #
111
+ # @param path [String] path name
112
+ #
113
+ # @since 0.1.0
114
+ # @api public
115
+ def initialize(path)
116
+ super("not a memory file `#{path}'")
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,377 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Dry
6
+ class Files
7
+ # File System abstraction to support `Dry::Files`
8
+ #
9
+ # @since 0.1.0
10
+ # @api private
11
+ class FileSystem
12
+ # @since 0.1.0
13
+ # @api private
14
+ attr_reader :file
15
+
16
+ # @since 0.1.0
17
+ # @api private
18
+ attr_reader :file_utils
19
+
20
+ # Creates a new instance
21
+ #
22
+ # @param file [Class]
23
+ # @param file_utils [Class]
24
+ #
25
+ # @return [Dry::Files::FileSystem]
26
+ #
27
+ # @since 0.1.0
28
+ def initialize(file: File, file_utils: FileUtils)
29
+ @file = file
30
+ @file_utils = file_utils
31
+ end
32
+
33
+ # Opens (or creates) a new file for both read/write operations.
34
+ #
35
+ # If the file doesn't exist, it creates a new one.
36
+ #
37
+ # @see https://ruby-doc.org/core/File.html#method-c-open
38
+ #
39
+ # @param path [String] the target file
40
+ # @param mode [String,Integer] Ruby file open mode
41
+ # @param args [Array<Object>] ::File.open args
42
+ # @param blk [Proc] the block to yield
43
+ #
44
+ # @yieldparam [::File] the opened file
45
+ #
46
+ # @raise [Dry::Files::IOError] in case of I/O error
47
+ #
48
+ # @since 0.1.0
49
+ def open(path, mode = OPEN_MODE, *args, &blk)
50
+ touch(path)
51
+
52
+ with_error_handling do
53
+ file.open(path, mode, *args, &blk)
54
+ end
55
+ end
56
+
57
+ # Opens the file, optionally seeks to the given offset, then returns
58
+ # length bytes (defaulting to the rest of the file).
59
+ #
60
+ # Read ensures the file is closed before returning.
61
+ #
62
+ # @see https://ruby-doc.org/core/IO.html#method-c-read
63
+ #
64
+ # @param path [String, Array<String>] the target path
65
+ #
66
+ # @raise [Dry::Files::IOError] in case of I/O error
67
+ #
68
+ # @since 0.1.0
69
+ # @api private
70
+ def read(path, *args, **kwargs)
71
+ with_error_handling do
72
+ file.read(path, *args, **kwargs)
73
+ end
74
+ end
75
+
76
+ # Reads the entire file specified by name as individual lines,
77
+ # and returns those lines in an array
78
+ #
79
+ # @see https://ruby-doc.org/core/IO.html#method-c-readlines
80
+ #
81
+ # @param path [String] the file to read
82
+ #
83
+ # @raise [Dry::Files::IOError] in case of I/O error
84
+ #
85
+ # @since 0.1.0
86
+ # @api private
87
+ def readlines(path, *args)
88
+ with_error_handling do
89
+ file.readlines(path, *args)
90
+ end
91
+ end
92
+
93
+ # Updates modification time (mtime) and access time (atime) of file(s)
94
+ # in list.
95
+ #
96
+ # Files are created if they don’t exist.
97
+ #
98
+ # @see https://ruby-doc.org/stdlib/libdoc/fileutils/rdoc/FileUtils.html#method-c-touch
99
+ #
100
+ # @param path [String, Array<String>] the target path
101
+ #
102
+ # @raise [Dry::Files::IOError] in case of I/O error
103
+ #
104
+ # @since 0.1.0
105
+ # @api private
106
+ def touch(path, **kwargs)
107
+ raise IOError, Errno::EISDIR.new(path.to_s) if directory?(path)
108
+
109
+ with_error_handling do
110
+ mkdir_p(path)
111
+ file_utils.touch(path, **kwargs)
112
+ end
113
+ end
114
+
115
+ # Creates a new file or rewrites the contents
116
+ # of an existing file for the given path and content
117
+ # All the intermediate directories are created.
118
+ #
119
+ # @param path [String,Pathname] the path to file
120
+ # @param content [String, Array<String>] the content to write
121
+ #
122
+ # @raise [Dry::Files::IOError] in case of I/O error
123
+ #
124
+ # @since 0.1.0
125
+ # @api private
126
+ def write(path, *content)
127
+ mkdir_p(path)
128
+
129
+ self.open(path, WRITE_MODE) do |f|
130
+ f.write(Array(content).flatten.join)
131
+ end
132
+ end
133
+
134
+ # Returns a new string formed by joining the strings using Operating
135
+ # System path separator
136
+ #
137
+ # @see https://ruby-doc.org/core/File.html#method-c-join
138
+ #
139
+ # @param path [Array<String,Pathname>] path tokens
140
+ #
141
+ # @return [String] the joined path
142
+ #
143
+ # @since 0.1.0
144
+ # @api private
145
+ def join(*path)
146
+ file.join(*path)
147
+ end
148
+
149
+ # Converts a path to an absolute path.
150
+ #
151
+ # @see https://ruby-doc.org/core/File.html#method-c-expand_path
152
+ #
153
+ # @param path [String,Pathname] the path to the file
154
+ # @param dir [String,Pathname] the base directory
155
+ #
156
+ # @since 0.1.0
157
+ # @api private
158
+ def expand_path(path, dir)
159
+ file.expand_path(path, dir)
160
+ end
161
+
162
+ # Returns the name of the current working directory.
163
+ #
164
+ # @see https://ruby-doc.org/stdlib/libdoc/fileutils/rdoc/FileUtils.html#method-c-pwd
165
+ #
166
+ # @return [String] the current working directory.
167
+ #
168
+ # @since 0.1.0
169
+ # @api private
170
+ def pwd
171
+ file_utils.pwd
172
+ end
173
+
174
+ # Temporary changes the current working directory of the process to the
175
+ # given path and yield the given block.
176
+ #
177
+ # The argument `path` is intended to be a **directory**.
178
+ #
179
+ # @see https://ruby-doc.org/stdlib-3.0.0/libdoc/fileutils/rdoc/FileUtils.html#method-i-cd
180
+ #
181
+ # @param path [String,Pathname] the target directory
182
+ # @param blk [Proc] the code to execute with the target directory
183
+ #
184
+ # @raise [Dry::Files::IOError] in case of I/O error
185
+ #
186
+ # @since 0.1.0
187
+ # @api private
188
+ def chdir(path, &blk)
189
+ with_error_handling do
190
+ file_utils.chdir(path, &blk)
191
+ end
192
+ end
193
+
194
+ # Creates a directory and all its parent directories.
195
+ #
196
+ # The argument `path` is intended to be a **directory** that you want to
197
+ # explicitly create.
198
+ #
199
+ # @see #mkdir_p
200
+ # @see https://ruby-doc.org/stdlib/libdoc/fileutils/rdoc/FileUtils.html#method-c-mkdir_p
201
+ #
202
+ # @param path [String] the directory to create
203
+ #
204
+ # @raise [Dry::Files::IOError] in case of I/O error
205
+ #
206
+ # @example
207
+ # require "dry/cli/utils/files/file_system"
208
+ #
209
+ # fs = Dry::Files::FileSystem.new
210
+ # fs.mkdir("/usr/var/project")
211
+ # # creates all the directory structure (/usr/var/project)
212
+ #
213
+ # @since 0.1.0
214
+ # @api private
215
+ def mkdir(path, **kwargs)
216
+ with_error_handling do
217
+ file_utils.mkdir_p(path, **kwargs)
218
+ end
219
+ end
220
+
221
+ # Creates a directory and all its parent directories.
222
+ #
223
+ # The argument `path` is intended to be a **file**, where its
224
+ # directory ancestors will be implicitly created.
225
+ #
226
+ # @see #mkdir
227
+ # @see https://ruby-doc.org/stdlib/libdoc/fileutils/rdoc/FileUtils.html#method-c-mkdir
228
+ #
229
+ # @param path [String] the file that will be in the directories that
230
+ # this method creates
231
+ #
232
+ # @raise [Dry::Files::IOError] in case of I/O error
233
+ #
234
+ # @example
235
+ # require "dry/cli/utils/files/file_system"
236
+ #
237
+ # fs = Dry::Files::FileSystem.new
238
+ # fs.mkdir("/usr/var/project/file.rb")
239
+ # # creates all the directory structure (/usr/var/project)
240
+ # # where file.rb will eventually live
241
+ #
242
+ # @since 0.1.0
243
+ # @api private
244
+ def mkdir_p(path, **kwargs)
245
+ mkdir(
246
+ file.dirname(path), **kwargs
247
+ )
248
+ end
249
+
250
+ # Copies file content from `source` to `destination`
251
+ # All the intermediate `destination` directories are created.
252
+ #
253
+ # @see https://ruby-doc.org/stdlib/libdoc/fileutils/rdoc/FileUtils.html#method-c-cp
254
+ #
255
+ # @param source [String] the file(s) or directory to copy
256
+ # @param destination [String] the directory destination
257
+ #
258
+ # @raise [Dry::Files::IOError] in case of I/O error
259
+ #
260
+ # @since 0.1.0
261
+ # @api private
262
+ def cp(source, destination, **kwargs)
263
+ mkdir_p(destination)
264
+
265
+ with_error_handling do
266
+ file_utils.cp(source, destination, **kwargs)
267
+ end
268
+ end
269
+
270
+ # Removes (deletes) a file
271
+ #
272
+ # @see https://ruby-doc.org/stdlib/libdoc/fileutils/rdoc/FileUtils.html#method-c-rm
273
+ #
274
+ # @see #rm_rf
275
+ #
276
+ # @param path [String] the file to remove
277
+ #
278
+ # @raise [Dry::Files::IOError] in case of I/O error
279
+ #
280
+ # @since 0.1.0
281
+ # @api private
282
+ def rm(path, **kwargs)
283
+ with_error_handling do
284
+ file_utils.rm(path, **kwargs)
285
+ end
286
+ end
287
+
288
+ # Removes (deletes) a directory
289
+ #
290
+ # @see https://ruby-doc.org/stdlib/libdoc/fileutils/rdoc/FileUtils.html#method-c-remove_entry_secure
291
+ #
292
+ # @param path [String] the directory to remove
293
+ #
294
+ # @raise [Dry::Files::IOError] in case of I/O error
295
+ #
296
+ # @since 0.1.0
297
+ # @api private
298
+ def rm_rf(path, *args)
299
+ with_error_handling do
300
+ file_utils.remove_entry_secure(path, *args)
301
+ end
302
+ end
303
+
304
+ # Check if the given path exist.
305
+ #
306
+ # @see https://ruby-doc.org/core/File.html#method-c-exist-3F
307
+ #
308
+ # @param path [String,Pathname] the file to check
309
+ #
310
+ # @return [TrueClass,FalseClass] the result of the check
311
+ #
312
+ # @since 0.1.0
313
+ # @api private
314
+ def exist?(path)
315
+ file.exist?(path)
316
+ end
317
+
318
+ # Check if the given path is a directory.
319
+ #
320
+ # @see https://ruby-doc.org/core/File.html#method-c-directory-3F
321
+ #
322
+ # @param path [String,Pathname] the directory to check
323
+ #
324
+ # @return [TrueClass,FalseClass] the result of the check
325
+ #
326
+ # @since 0.1.0
327
+ # @api private
328
+ def directory?(path)
329
+ file.directory?(path)
330
+ end
331
+
332
+ # Check if the given path is an executable.
333
+ #
334
+ # @see https://ruby-doc.org/core/File.html#method-c-executable-3F
335
+ #
336
+ # @param path [String,Pathname] the path to check
337
+ #
338
+ # @return [TrueClass,FalseClass] the result of the check
339
+ #
340
+ # @since 0.1.0
341
+ # @api private
342
+ def executable?(path)
343
+ file.executable?(path)
344
+ end
345
+
346
+ private
347
+
348
+ # @since 0.1.0
349
+ # @api private
350
+ OPEN_MODE = ::File::RDWR
351
+ private_constant :OPEN_MODE
352
+
353
+ # @since 0.1.0
354
+ # @api private
355
+ WRITE_MODE = (::File::CREAT | ::File::WRONLY | ::File::TRUNC).freeze
356
+ private_constant :WRITE_MODE
357
+
358
+ # Catch `SystemCallError` and re-raise a `Dry::Files::IOError`.
359
+ #
360
+ # `SystemCallError` is parent for all the `Errno::*` Ruby exceptions.
361
+ # These class of exceptions are raised in case of I/O error.
362
+ #
363
+ # @see https://ruby-doc.org/core/SystemCallError.html
364
+ # @see https://ruby-doc.org/core/Errno.html
365
+ #
366
+ # @raise [Dry::Files::IOError] in case of I/O error
367
+ #
368
+ # @since 0.1.0
369
+ # @api private
370
+ def with_error_handling
371
+ yield
372
+ rescue SystemCallError => e
373
+ raise IOError, e
374
+ end
375
+ end
376
+ end
377
+ end