dry-files 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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