memfs 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +128 -0
- data/Rakefile +6 -0
- data/lib/fileutils.rb +1738 -0
- data/lib/memfs/dir.rb +33 -0
- data/lib/memfs/fake/directory.rb +51 -0
- data/lib/memfs/fake/entry.rb +73 -0
- data/lib/memfs/fake/file/content.rb +47 -0
- data/lib/memfs/fake/file.rb +36 -0
- data/lib/memfs/fake/symlink.rb +32 -0
- data/lib/memfs/file/stat.rb +48 -0
- data/lib/memfs/file.rb +284 -0
- data/lib/memfs/file_system.rb +157 -0
- data/lib/memfs/filesystem_access.rb +7 -0
- data/lib/memfs/version.rb +3 -0
- data/lib/memfs.rb +100 -0
- data/memfs.gemspec +29 -0
- data/spec/fileutils_spec.rb +961 -0
- data/spec/memfs/dir_spec.rb +104 -0
- data/spec/memfs/fake/directory_spec.rb +115 -0
- data/spec/memfs/fake/entry_spec.rb +140 -0
- data/spec/memfs/fake/file/content_spec.rb +154 -0
- data/spec/memfs/fake/file_spec.rb +37 -0
- data/spec/memfs/fake/symlink_spec.rb +43 -0
- data/spec/memfs/file/stat_spec.rb +209 -0
- data/spec/memfs/file_spec.rb +928 -0
- data/spec/memfs/file_system_spec.rb +393 -0
- data/spec/memfs_spec.rb +54 -0
- data/spec/spec_helper.rb +20 -0
- metadata +203 -0
data/lib/memfs/dir.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'memfs/filesystem_access'
|
2
|
+
|
3
|
+
module MemFs
|
4
|
+
class Dir
|
5
|
+
extend FilesystemAccess
|
6
|
+
|
7
|
+
def self.chdir(path, &block)
|
8
|
+
fs.chdir path, &block
|
9
|
+
return 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.entries(dirname, opts = {})
|
13
|
+
fs.entries(dirname)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.exists?(path)
|
17
|
+
fs.directory?(path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.getwd
|
21
|
+
fs.getwd
|
22
|
+
end
|
23
|
+
class << self; alias :pwd :getwd; end
|
24
|
+
|
25
|
+
def self.mkdir(path)
|
26
|
+
fs.mkdir path
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.rmdir(path)
|
30
|
+
fs.rmdir path
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'memfs/fake/entry'
|
2
|
+
|
3
|
+
module MemFs
|
4
|
+
module Fake
|
5
|
+
class Directory < Entry
|
6
|
+
attr_accessor :entries
|
7
|
+
|
8
|
+
def add_entry(entry)
|
9
|
+
entries[entry.name] = entry
|
10
|
+
entry.parent = self
|
11
|
+
end
|
12
|
+
|
13
|
+
def empty?
|
14
|
+
(entries.keys - %w[. ..]).empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
def entry_names
|
18
|
+
entries.keys
|
19
|
+
end
|
20
|
+
|
21
|
+
def find(path)
|
22
|
+
path = path.sub(/\A\/+/, '')
|
23
|
+
parts = path.split('/', 2)
|
24
|
+
|
25
|
+
if entry_names.include?(path)
|
26
|
+
entries[path]
|
27
|
+
elsif entry_names.include?(parts.first)
|
28
|
+
entries[parts.first].find(parts.last)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(*args)
|
33
|
+
super
|
34
|
+
self.entries = { '.' => self, '..' => nil }
|
35
|
+
end
|
36
|
+
|
37
|
+
def parent=(parent)
|
38
|
+
super
|
39
|
+
entries['..'] = parent
|
40
|
+
end
|
41
|
+
|
42
|
+
def path
|
43
|
+
name == '/' ? '/' : super
|
44
|
+
end
|
45
|
+
|
46
|
+
def remove_entry(entry)
|
47
|
+
entries.delete(entry.name)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module MemFs
|
2
|
+
module Fake
|
3
|
+
class Entry
|
4
|
+
UREAD = 00100
|
5
|
+
UWRITE = 00200
|
6
|
+
UEXEC = 00400
|
7
|
+
GREAD = 00010
|
8
|
+
GWRITE = 00020
|
9
|
+
GEXEC = 00040
|
10
|
+
OREAD = 00001
|
11
|
+
OWRITE = 00002
|
12
|
+
OEXEC = 00004
|
13
|
+
RSTICK = 01000
|
14
|
+
USTICK = 05000
|
15
|
+
|
16
|
+
attr_accessor :atime,
|
17
|
+
:gid,
|
18
|
+
:mtime,
|
19
|
+
:name,
|
20
|
+
:parent,
|
21
|
+
:uid
|
22
|
+
attr_reader :mode
|
23
|
+
|
24
|
+
def blksize
|
25
|
+
4096
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete
|
29
|
+
parent.remove_entry self
|
30
|
+
end
|
31
|
+
|
32
|
+
def dereferenced
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def dev
|
37
|
+
@dev ||= rand(1000)
|
38
|
+
end
|
39
|
+
|
40
|
+
def find(path)
|
41
|
+
raise Errno::ENOTDIR, self.path
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(path = nil)
|
45
|
+
current_user = Etc.getpwuid
|
46
|
+
time = Time.now
|
47
|
+
self.atime = time
|
48
|
+
self.gid = current_user.gid
|
49
|
+
self.mode = 0666 - MemFs::File.umask
|
50
|
+
self.mtime = time
|
51
|
+
self.name = MemFs::File.basename(path || '')
|
52
|
+
self.uid = current_user.uid
|
53
|
+
end
|
54
|
+
|
55
|
+
def ino
|
56
|
+
@ino ||= rand(1000)
|
57
|
+
end
|
58
|
+
|
59
|
+
def mode=(mode_int)
|
60
|
+
@mode = 0100000 | mode_int
|
61
|
+
end
|
62
|
+
|
63
|
+
def path
|
64
|
+
parts = [parent && parent.path, name].compact
|
65
|
+
MemFs::File.join(parts)
|
66
|
+
end
|
67
|
+
|
68
|
+
def touch
|
69
|
+
self.atime = self.mtime = Time.now
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "delegate"
|
2
|
+
|
3
|
+
module MemFs
|
4
|
+
module Fake
|
5
|
+
class File < Entry
|
6
|
+
|
7
|
+
class Content < SimpleDelegator
|
8
|
+
attr_accessor :pos
|
9
|
+
|
10
|
+
def close
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(obj = '')
|
14
|
+
@string = obj.to_s.dup
|
15
|
+
@pos = 0
|
16
|
+
|
17
|
+
__setobj__ @string
|
18
|
+
end
|
19
|
+
|
20
|
+
def puts(*strings)
|
21
|
+
strings.each do |str|
|
22
|
+
@string << str
|
23
|
+
@string << $/ unless str.end_with?($/)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def read(length = nil, buffer = '')
|
28
|
+
length ||= @string.length - @pos
|
29
|
+
buffer.replace @string[@pos, length]
|
30
|
+
@pos += buffer.bytesize
|
31
|
+
buffer.empty? ? nil : buffer
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
@string
|
36
|
+
end
|
37
|
+
|
38
|
+
def write(string)
|
39
|
+
text = string.to_s
|
40
|
+
@string << text
|
41
|
+
text.size
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'memfs/fake/entry'
|
2
|
+
require 'memfs/fake/file/content'
|
3
|
+
|
4
|
+
module MemFs
|
5
|
+
module Fake
|
6
|
+
class File < Entry
|
7
|
+
attr_accessor :content
|
8
|
+
|
9
|
+
def close
|
10
|
+
@closed = true
|
11
|
+
end
|
12
|
+
|
13
|
+
def closed?
|
14
|
+
@closed
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(*args)
|
18
|
+
super
|
19
|
+
@content = Content.new
|
20
|
+
@closed = false
|
21
|
+
end
|
22
|
+
|
23
|
+
def pos
|
24
|
+
content.pos
|
25
|
+
end
|
26
|
+
|
27
|
+
def pos=(value)
|
28
|
+
content.pos = value
|
29
|
+
end
|
30
|
+
|
31
|
+
def size
|
32
|
+
content.size
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'memfs/filesystem_access'
|
2
|
+
|
3
|
+
module MemFs
|
4
|
+
module Fake
|
5
|
+
class Symlink < Entry
|
6
|
+
include MemFs::FilesystemAccess
|
7
|
+
|
8
|
+
attr_reader :target
|
9
|
+
|
10
|
+
def dereferenced
|
11
|
+
@dereferenced ||= fs.find!(target).dereferenced
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(path, target)
|
15
|
+
super(path)
|
16
|
+
@target = target
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(meth, *args, &block)
|
20
|
+
if dereferenced.respond_to?(meth)
|
21
|
+
dereferenced.public_send(meth, *args, &block)
|
22
|
+
else
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def respond_to_missing?(meth, include_private)
|
28
|
+
dereferenced.respond_to?(meth, include_private) || super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'memfs/filesystem_access'
|
3
|
+
|
4
|
+
module MemFs
|
5
|
+
class File
|
6
|
+
class Stat
|
7
|
+
extend Forwardable
|
8
|
+
include FilesystemAccess
|
9
|
+
|
10
|
+
attr_reader :entry
|
11
|
+
|
12
|
+
def_delegators :entry,
|
13
|
+
:atime,
|
14
|
+
:blksize,
|
15
|
+
:dev,
|
16
|
+
:gid,
|
17
|
+
:ino,
|
18
|
+
:mode,
|
19
|
+
:mtime,
|
20
|
+
:uid
|
21
|
+
|
22
|
+
def directory?
|
23
|
+
File.directory? entry.path
|
24
|
+
end
|
25
|
+
|
26
|
+
def file?
|
27
|
+
File.file? entry.path
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(path, dereference = false)
|
31
|
+
entry = fs.find!(path)
|
32
|
+
@entry = dereference ? entry.dereferenced : entry
|
33
|
+
end
|
34
|
+
|
35
|
+
def sticky?
|
36
|
+
!!(entry.mode & Fake::Entry::USTICK).nonzero?
|
37
|
+
end
|
38
|
+
|
39
|
+
def symlink?
|
40
|
+
File.symlink? entry.path
|
41
|
+
end
|
42
|
+
|
43
|
+
def world_writable?
|
44
|
+
entry.mode if (entry.mode & Fake::Entry::OWRITE).nonzero?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/memfs/file.rb
ADDED
@@ -0,0 +1,284 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'memfs/filesystem_access'
|
3
|
+
|
4
|
+
module MemFs
|
5
|
+
class File
|
6
|
+
extend FilesystemAccess
|
7
|
+
extend SingleForwardable
|
8
|
+
include FilesystemAccess
|
9
|
+
|
10
|
+
OriginalFile.constants.grep(/^[A-Z_]+$/).each do |const|
|
11
|
+
const_set const, OriginalFile.const_get(const)
|
12
|
+
end
|
13
|
+
|
14
|
+
MODE_MAP = {
|
15
|
+
'r' => RDONLY,
|
16
|
+
'r+' => RDWR,
|
17
|
+
'w' => CREAT|TRUNC|WRONLY,
|
18
|
+
'w+' => CREAT|TRUNC|RDWR,
|
19
|
+
'a' => CREAT|APPEND|WRONLY,
|
20
|
+
'a+' => CREAT|APPEND|RDWR
|
21
|
+
}
|
22
|
+
|
23
|
+
SUCCESS = 0
|
24
|
+
|
25
|
+
def_delegators :original_file_class,
|
26
|
+
:basename,
|
27
|
+
:dirname,
|
28
|
+
:join,
|
29
|
+
:path
|
30
|
+
|
31
|
+
def self.atime(path)
|
32
|
+
stat(path).atime
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.chmod(mode_int, *paths)
|
36
|
+
paths.each do |path|
|
37
|
+
fs.chmod mode_int, path
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.chown(uid, gid, *paths)
|
42
|
+
paths.each do |path|
|
43
|
+
fs.chown(uid, gid, path)
|
44
|
+
end
|
45
|
+
paths.size
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.directory?(path)
|
49
|
+
fs.directory? path
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.exists?(path)
|
53
|
+
not fs.find(path).nil?
|
54
|
+
end
|
55
|
+
class << self; alias :exist? :exists?; end
|
56
|
+
|
57
|
+
def self.expand_path(file_name, dir_string = fs.pwd)
|
58
|
+
original_file_class.expand_path(file_name, dir_string)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.file?(path)
|
62
|
+
fs.find(path).is_a?(Fake::File)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.identical?(path1, path2)
|
66
|
+
fs.find!(path1).dereferenced === fs.find!(path2).dereferenced
|
67
|
+
rescue Errno::ENOENT
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.lchmod(mode_int, *file_names)
|
72
|
+
file_names.each do |file_name|
|
73
|
+
fs.chmod mode_int, file_name
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.lchown(uid, gid, *paths)
|
78
|
+
chown uid, gid, *paths
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.link(old_name, new_name)
|
82
|
+
fs.link old_name, new_name
|
83
|
+
SUCCESS
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.lstat(path)
|
87
|
+
Stat.new(path)
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.mtime(path)
|
91
|
+
stat(path).mtime
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.open(filename, mode = RDONLY, *perm_and_opt)
|
95
|
+
file = self.new(filename, mode, *perm_and_opt)
|
96
|
+
|
97
|
+
if block_given?
|
98
|
+
yield file
|
99
|
+
else
|
100
|
+
file
|
101
|
+
end
|
102
|
+
ensure
|
103
|
+
file.close if file && block_given?
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.read(path, length = nil, offset = 0, mode: RDONLY, encoding: nil, open_args: nil)
|
107
|
+
open_args ||= [mode, encoding: encoding]
|
108
|
+
|
109
|
+
file = open(path, *open_args)
|
110
|
+
file.seek(offset)
|
111
|
+
file.read(length)
|
112
|
+
ensure
|
113
|
+
file.close if file
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.readlink(path)
|
117
|
+
fs.find!(path).target
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.rename(old_name, new_name)
|
121
|
+
fs.rename(old_name, new_name)
|
122
|
+
SUCCESS
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.reset!
|
126
|
+
@umask = original_file_class.umask
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.size(path)
|
130
|
+
fs.find!(path).size
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.stat(path)
|
134
|
+
Stat.new(path, true)
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.symlink(old_name, new_name)
|
138
|
+
fs.symlink old_name, new_name
|
139
|
+
SUCCESS
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.symlink?(path)
|
143
|
+
fs.symlink? path
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.umask(integer = nil)
|
147
|
+
old_value = @umask || original_file_class.umask
|
148
|
+
|
149
|
+
@umask = integer if integer
|
150
|
+
|
151
|
+
old_value
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.unlink(*paths)
|
155
|
+
paths.each do |path|
|
156
|
+
fs.unlink(path)
|
157
|
+
end
|
158
|
+
paths.size
|
159
|
+
end
|
160
|
+
class << self; alias :delete :unlink; end
|
161
|
+
|
162
|
+
def self.utime(atime, mtime, *file_names)
|
163
|
+
file_names.each do |file_name|
|
164
|
+
fs.find!(file_name).atime = atime
|
165
|
+
fs.find!(file_name).mtime = mtime
|
166
|
+
end
|
167
|
+
file_names.size
|
168
|
+
end
|
169
|
+
|
170
|
+
attr_accessor :closed,
|
171
|
+
:entry,
|
172
|
+
:opening_mode
|
173
|
+
attr_reader :path
|
174
|
+
|
175
|
+
def initialize(filename, mode = RDONLY, perm = nil, opt = nil)
|
176
|
+
unless opt.nil? || opt.is_a?(Hash)
|
177
|
+
raise ArgumentError, "wrong number of arguments (4 for 1..3)"
|
178
|
+
end
|
179
|
+
|
180
|
+
@path = filename
|
181
|
+
|
182
|
+
self.opening_mode = str_to_mode_int(mode)
|
183
|
+
|
184
|
+
fs.touch(filename) if create_file?
|
185
|
+
|
186
|
+
self.entry = fs.find(filename)
|
187
|
+
end
|
188
|
+
|
189
|
+
def chmod(mode_int)
|
190
|
+
fs.chmod(mode_int, path)
|
191
|
+
SUCCESS
|
192
|
+
end
|
193
|
+
|
194
|
+
def chown(uid, gid = nil)
|
195
|
+
fs.chown(uid, gid, path)
|
196
|
+
SUCCESS
|
197
|
+
end
|
198
|
+
|
199
|
+
def close
|
200
|
+
self.closed = true
|
201
|
+
end
|
202
|
+
|
203
|
+
def closed?
|
204
|
+
closed
|
205
|
+
end
|
206
|
+
|
207
|
+
def content
|
208
|
+
entry.content
|
209
|
+
end
|
210
|
+
|
211
|
+
def lstat
|
212
|
+
File.lstat(path)
|
213
|
+
end
|
214
|
+
|
215
|
+
def pos
|
216
|
+
entry.pos
|
217
|
+
end
|
218
|
+
|
219
|
+
def puts(text)
|
220
|
+
raise IOError, 'not opened for writing' unless writable?
|
221
|
+
|
222
|
+
content.puts text
|
223
|
+
end
|
224
|
+
|
225
|
+
def read(length = nil, buffer = '')
|
226
|
+
default = length ? nil : ''
|
227
|
+
content.read(length, buffer) || default
|
228
|
+
end
|
229
|
+
|
230
|
+
def seek(amount, whence = IO::SEEK_SET)
|
231
|
+
new_pos = case whence
|
232
|
+
when IO::SEEK_CUR then entry.pos + amount
|
233
|
+
when IO::SEEK_END then content.to_s.length + amount
|
234
|
+
when IO::SEEK_SET then amount
|
235
|
+
end
|
236
|
+
|
237
|
+
if new_pos.nil? || new_pos < 0
|
238
|
+
raise Errno::EINVAL, path
|
239
|
+
end
|
240
|
+
|
241
|
+
entry.pos = new_pos and 0
|
242
|
+
end
|
243
|
+
|
244
|
+
def size
|
245
|
+
entry.size
|
246
|
+
end
|
247
|
+
|
248
|
+
def stat
|
249
|
+
File.stat(path)
|
250
|
+
end
|
251
|
+
|
252
|
+
def write(string)
|
253
|
+
raise IOError, 'not opened for writing' unless writable?
|
254
|
+
|
255
|
+
content.write(string.to_s)
|
256
|
+
end
|
257
|
+
|
258
|
+
private
|
259
|
+
|
260
|
+
def self.original_file_class
|
261
|
+
MemFs::OriginalFile
|
262
|
+
end
|
263
|
+
|
264
|
+
def str_to_mode_int(mode)
|
265
|
+
return mode unless mode.is_a?(String)
|
266
|
+
|
267
|
+
unless mode =~ /\A([rwa]\+?)([bt])?\z/
|
268
|
+
raise ArgumentError, "invalid access mode #{mode}"
|
269
|
+
end
|
270
|
+
|
271
|
+
mode_str = $~[1]
|
272
|
+
MODE_MAP[mode_str]
|
273
|
+
end
|
274
|
+
|
275
|
+
def create_file?
|
276
|
+
(opening_mode & File::CREAT).nonzero?
|
277
|
+
end
|
278
|
+
|
279
|
+
def writable?
|
280
|
+
(opening_mode & File::WRONLY).nonzero? ||
|
281
|
+
(opening_mode & File::RDWR).nonzero?
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|