io 0.0.1
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.
- data/Rakefile +10 -0
- data/lib/vfs.rb +20 -0
- data/lib/vfs/drivers/local.rb +175 -0
- data/lib/vfs/drivers/specification.rb +169 -0
- data/lib/vfs/entries/dir.rb +253 -0
- data/lib/vfs/entries/entry.rb +147 -0
- data/lib/vfs/entries/file.rb +154 -0
- data/lib/vfs/entries/universal_entry.rb +24 -0
- data/lib/vfs/entry_proxy.rb +42 -0
- data/lib/vfs/error.rb +4 -0
- data/lib/vfs/integration.rb +30 -0
- data/lib/vfs/path.rb +125 -0
- data/lib/vfs/vfs.rb +38 -0
- data/readme.md +119 -0
- data/spec/container_spec.rb +31 -0
- data/spec/dir_spec.rb +249 -0
- data/spec/entry_spec.rb +42 -0
- data/spec/file_spec.rb +210 -0
- data/spec/misc_spec.rb +19 -0
- data/spec/path_spec.rb +125 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/storages/local_spec.rb +24 -0
- data/spec/storages/local_spec/emptygit +0 -0
- data/spec/universal_entry_spec.rb +73 -0
- metadata +68 -0
data/lib/vfs/vfs.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module Vfs
|
2
|
+
class << self
|
3
|
+
def default_driver
|
4
|
+
::Vfs::Drivers::Local.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_entry
|
8
|
+
'/'.to_entry
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_file
|
12
|
+
to_entry.file
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_dir
|
16
|
+
to_entry.dir
|
17
|
+
end
|
18
|
+
|
19
|
+
# def [] path
|
20
|
+
# to_entry[path]
|
21
|
+
# end
|
22
|
+
# alias_method :/, :[]
|
23
|
+
|
24
|
+
%w(
|
25
|
+
entry dir file
|
26
|
+
entries dirs files
|
27
|
+
[] /
|
28
|
+
tmp
|
29
|
+
).each do |m|
|
30
|
+
script = <<-RUBY
|
31
|
+
def #{m} *a, &b
|
32
|
+
to_entry.#{m} *a, &b
|
33
|
+
end
|
34
|
+
RUBY
|
35
|
+
eval script, binding, __FILE__, __LINE__
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/readme.md
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# Vfs - Virtual File System
|
2
|
+
|
3
|
+
Handy and simple abstraction over any storage that can represent concept of File and Directory (or at least part of it).
|
4
|
+
The Vfs for File System is kinda the same as ActiveRecord is for Relational Databases.
|
5
|
+
|
6
|
+
Currently, there are following implementations available:
|
7
|
+
|
8
|
+
- local file system
|
9
|
+
- remote file system (over ssh)
|
10
|
+
|
11
|
+
## Goals
|
12
|
+
|
13
|
+
- **handy, simple and clean** API.
|
14
|
+
- same API for different storages (Local FS, SSH, Hadoop, or any other , ...).
|
15
|
+
- should work **simultaneously with different storages**.
|
16
|
+
- small codebase, easy to extend by others.
|
17
|
+
- simple storage-driver implementation, easy add new storage types (Hadoop DFS, LDAP, Document Oriented DB, In-Memory, ...).
|
18
|
+
|
19
|
+
**Performance**:
|
20
|
+
|
21
|
+
- sometimes there's extra call to check if file or dir exist before overriding it
|
22
|
+
- copy: right now it doesn't use FileUtils.cp_r, it walks on the directory tree and copy each entry individually, so it's probably a little slover.
|
23
|
+
- right now :move and :rename implemented ASAP by copy & destroy, will be fixed as soon as I'll have time to do it.
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
|
27
|
+
``` bash
|
28
|
+
$ gem install vfs
|
29
|
+
$ gem install vos
|
30
|
+
```
|
31
|
+
|
32
|
+
## Code samples:
|
33
|
+
|
34
|
+
``` ruby
|
35
|
+
require 'vfs' # Virtual File System
|
36
|
+
require 'vos' # Virtual Operating System
|
37
|
+
|
38
|
+
# Connections, let's deploy our 'cool_app' project from our local box to remote server
|
39
|
+
|
40
|
+
server = Box.new('cool_app.com') # it will use id_rsa, or You can add {user: 'me', password: 'secret'}
|
41
|
+
me = '~'.to_dir # handy shortcut for local FS
|
42
|
+
|
43
|
+
deploy_dir = server['apps/cool_app']
|
44
|
+
projects = me['projects']
|
45
|
+
|
46
|
+
# Working with dirs, copying dir from any source to any destination (local/remote/custom_storage_type)
|
47
|
+
|
48
|
+
projects['cool_app'].copy_to deploy_dir
|
49
|
+
|
50
|
+
# Working with files
|
51
|
+
|
52
|
+
dbc = deploy_dir.file('config/database.yml') # <= the 'config' dir not exist yet
|
53
|
+
dbc.write("user: root\npassword: secret") # <= now the 'database.yml' and parent 'config' has been created
|
54
|
+
dbc.read =~ /database/ # => false, we forgot to add the database
|
55
|
+
dbc.append("\ndatabase: mysql") # let's do it
|
56
|
+
|
57
|
+
dbc.update do |content| # and add host info
|
58
|
+
content + "\nhost: cool_app.com "
|
59
|
+
end
|
60
|
+
|
61
|
+
projects['cool_app/config/database.yml']. # or just overwrite it with our local dev version
|
62
|
+
copy_to! dbc
|
63
|
+
```
|
64
|
+
|
65
|
+
There are also streaming support (read/write/append) with &block, please go to specs for details
|
66
|
+
|
67
|
+
# Checks
|
68
|
+
|
69
|
+
``` ruby
|
70
|
+
deploy_dir['config'].exist? # => true
|
71
|
+
deploy_dir.dir('config').exist? # => true
|
72
|
+
deploy_dir.file('config').exist? # => false
|
73
|
+
|
74
|
+
deploy_dir['config'].dir? # => true
|
75
|
+
deploy_dir['config'].file? # => false
|
76
|
+
```
|
77
|
+
|
78
|
+
# Navigation
|
79
|
+
|
80
|
+
``` ruby
|
81
|
+
config = deploy_dir['config']
|
82
|
+
config.parent # => </apps/cool_app>
|
83
|
+
config['../..'] # => </>
|
84
|
+
config['../..'].dir? # => true
|
85
|
+
|
86
|
+
deploy_dir.entries # => list of dirs and files, also support &block
|
87
|
+
deploy_dir.files # => list of files, also support &block
|
88
|
+
deploy_dir.dirs # => list of dirs, also support &block
|
89
|
+
```
|
90
|
+
|
91
|
+
For more please go to specs (create/update/move/copy/destroy/...)
|
92
|
+
|
93
|
+
## Integration with [Vos][vos] (Virtual Operating System)
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
server['apps/cool_app'].bash 'rails production'
|
97
|
+
```
|
98
|
+
|
99
|
+
For more details please go to [Vos][vos] project page.
|
100
|
+
Or checkout configuration I use to control my production servers [My Cluster][my_cluster] in conjunction with small
|
101
|
+
configuration tool [Cluster Management][cluster_management].
|
102
|
+
|
103
|
+
# Why?
|
104
|
+
|
105
|
+
To easy my work: with local FS, remote FS (cluster management, deployment automation), and some specific systems like Hadoop DFS.
|
106
|
+
|
107
|
+
Because the API of standard File/Dir/FileUtils classes are just terrible. And there's the reason for it - the goal of thouse tools
|
108
|
+
is to provide 1-to-1 clone of underlying OS API, instead of provididing handy tool.
|
109
|
+
|
110
|
+
And if you want to use remote FS - things are getting even worse and more complicated (Net::SSH & Net::SFTP use a little
|
111
|
+
different API than local FS, and you has to remember all thouse little quirks).
|
112
|
+
|
113
|
+
## License
|
114
|
+
|
115
|
+
Copyright (c) Alexey Petrushin http://petrush.in, released under the MIT license.
|
116
|
+
|
117
|
+
[vos]: http://github.com/alexeypetrushin/vos
|
118
|
+
[cluster_management]: http://github.com/alexeypetrushin/cluster_management
|
119
|
+
[my_cluster]: http://github.com/alexeypetrushin/my_cluster
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Container' do
|
4
|
+
with_test_dir
|
5
|
+
|
6
|
+
it "should threat paths as UniversalEntry except it ends with '/'" do
|
7
|
+
test_dir.should_receive(:entry).with('tmp/a/b')
|
8
|
+
test_dir['tmp/a/b']
|
9
|
+
|
10
|
+
test_dir.should_receive(:dir).with('tmp/a/b')
|
11
|
+
test_dir['tmp/a/b/']
|
12
|
+
end
|
13
|
+
|
14
|
+
it '/' do
|
15
|
+
test_dir[:some_path].should == test_dir / :some_path
|
16
|
+
test_dir[:some_path][:another_path].should == test_dir / :some_path / :another_path
|
17
|
+
end
|
18
|
+
|
19
|
+
it "UniversalEntry should be wrapped inside of proxy, Dir and File should not" do
|
20
|
+
-> {test_dir.dir.proxy?}.should raise_error(NoMethodError)
|
21
|
+
-> {test_dir.file.proxy?}.should raise_error(NoMethodError)
|
22
|
+
test_dir.entry.proxy?.should be_true
|
23
|
+
end
|
24
|
+
|
25
|
+
it "sometimes it also should inexplicitly guess that path is a Dir instead of UniversalEntry (but still wrap it inside of Proxy)" do
|
26
|
+
|
27
|
+
dir = test_dir['tmp/a/..']
|
28
|
+
dir.proxy?.should be_true
|
29
|
+
dir.should be_a(Vfs::Dir)
|
30
|
+
end
|
31
|
+
end
|
data/spec/dir_spec.rb
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Dir' do
|
4
|
+
with_test_dir
|
5
|
+
|
6
|
+
before do
|
7
|
+
@path = test_dir['a/b/c']
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'existence' do
|
11
|
+
it "should check only dirs" do
|
12
|
+
@path.should_not exist
|
13
|
+
@path.file.create
|
14
|
+
@path.should be_file
|
15
|
+
@path.dir.should_not exist
|
16
|
+
@path.dir.create
|
17
|
+
@path.should be_dir
|
18
|
+
@path.dir.should exist
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should not respond to read and write methods" do
|
23
|
+
-> {@path.dir.read}.should raise_error(NoMethodError)
|
24
|
+
-> {@path.dir.write}.should raise_error(NoMethodError)
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'create' do
|
28
|
+
it 'should be chainable' do
|
29
|
+
@path.dir.create.should == @path
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should create parent dirs if not exists' do
|
33
|
+
@path.parent.should_not exist
|
34
|
+
@path.dir.create
|
35
|
+
@path.should be_dir
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should silently exit if dir already exist' do
|
39
|
+
@path.dir.create
|
40
|
+
@path.dir.create
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should override existing file' do
|
44
|
+
@path.file.create
|
45
|
+
@path.should be_file
|
46
|
+
@path.dir.create
|
47
|
+
@path.should be_dir
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should not override existing dir with content' do
|
51
|
+
dir = @path.dir
|
52
|
+
dir.create
|
53
|
+
file = dir.file :file
|
54
|
+
file.create
|
55
|
+
file.should exist
|
56
|
+
|
57
|
+
dir.create
|
58
|
+
file.should exist
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe 'destroying' do
|
63
|
+
it "should destroy a file" do
|
64
|
+
@path.file.create
|
65
|
+
@path.dir.destroy
|
66
|
+
@path.entry.should_not exist
|
67
|
+
end
|
68
|
+
|
69
|
+
it "shouldn't raise if dir not exist" do
|
70
|
+
@path.dir.destroy
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should destroy recursivelly' do
|
74
|
+
dir = @path.dir
|
75
|
+
dir.create
|
76
|
+
dir.file(:file).write 'something'
|
77
|
+
dir.dir(:dir).create.tap do |dir|
|
78
|
+
dir.file(:file2).write 'something2'
|
79
|
+
end
|
80
|
+
|
81
|
+
dir.destroy
|
82
|
+
dir.should_not exist
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should be chainable' do
|
86
|
+
@path.dir.destroy.should == @path
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe 'entries, files, dirs' do
|
91
|
+
before do
|
92
|
+
@path.dir('dir').create
|
93
|
+
@path.dir('dir/another_dir').create
|
94
|
+
@path.file('file').create
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'entries' do
|
98
|
+
-> {@path['non_existing'].entries}.should raise_error(Vfs::Error, /not exist/)
|
99
|
+
@path['non_existing'].entries(bang: false).should == []
|
100
|
+
@path.entries.to_set.should be_eql([@path.entry('dir'), @path.entry('file')].to_set)
|
101
|
+
list = []
|
102
|
+
@path.entries{|e| list << e}
|
103
|
+
list.to_set.should be_eql([@path.entry('dir'), @path.entry('file')].to_set)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'entries with type' do
|
107
|
+
@path.entries(type: true).to_set.should be_eql([@path.dir('dir'), @path.file('file')].to_set)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "glob search support" do
|
111
|
+
@path.dir('dir_a').create
|
112
|
+
@path.file('file_a').create
|
113
|
+
@path.dir('dir_b').create
|
114
|
+
@path.entries('*_a').collect(&:name).sort.should == %w(dir_a file_a)
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'should raise error if trying :entries on file' do
|
118
|
+
@path.file('some_file').create
|
119
|
+
-> {@path.dir('some_file').entries}.should raise_error(/File/)
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'files' do
|
123
|
+
@path.files.should be_eql([@path.file('file')])
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'dirs' do
|
127
|
+
@path.dirs.should be_eql([@path.dir('dir')])
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'has? & include?' do
|
131
|
+
@path.include?('dir').should be_true
|
132
|
+
@path.include?('dir/another_dir').should be_true
|
133
|
+
@path.include?('file').should be_true
|
134
|
+
@path.include?('non_existing').should be_false
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'empty?' do
|
138
|
+
@path.empty?.should be_false
|
139
|
+
@path.dir('empty_dir').create.empty?.should be_true
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe 'copying' do
|
144
|
+
before do
|
145
|
+
@from = @path.dir
|
146
|
+
@from.create
|
147
|
+
@from.file('file').write 'something'
|
148
|
+
@from.dir('dir').create.tap do |dir|
|
149
|
+
dir.file('file2').write 'something2'
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'should not copy to itself' do
|
154
|
+
-> {@from.copy_to @from}.should raise_error(Vfs::Error, /itself/)
|
155
|
+
end
|
156
|
+
|
157
|
+
shared_examples_for 'copy_to behavior' do
|
158
|
+
it 'should copy to file and overwrite it' do
|
159
|
+
@from.copy_to @to.file
|
160
|
+
@to['file'].read.should == 'something'
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'should override files' do
|
164
|
+
@from.copy_to @to
|
165
|
+
|
166
|
+
@from['dir/file2'].write 'another'
|
167
|
+
@from.copy_to @to
|
168
|
+
@to['dir/file2'].read.should == 'another'
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'should copy to UniversalEntry (and overwrite)' do
|
172
|
+
@from.copy_to @to.entry
|
173
|
+
|
174
|
+
@from.copy_to @to.entry
|
175
|
+
@to['file'].read.should == 'something'
|
176
|
+
end
|
177
|
+
|
178
|
+
it "shouldn't delete existing content of directory" do
|
179
|
+
@to.dir.create
|
180
|
+
@to.file('existing_file').write 'existing_content'
|
181
|
+
@to.dir('existing_dir').create
|
182
|
+
@to.file('dir/existing_file2').write 'existing_content2'
|
183
|
+
|
184
|
+
@from.copy_to @to
|
185
|
+
# copied files
|
186
|
+
@to['file'].read.should == 'something'
|
187
|
+
@to['dir/file2'].read.should == 'something2'
|
188
|
+
# shouldn't delete already existing files
|
189
|
+
@to.file('existing_file').read.should == 'existing_content'
|
190
|
+
@to.dir('existing_dir').should exist
|
191
|
+
@to.file('dir/existing_file2').read.should == 'existing_content2'
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'should be chainable' do
|
195
|
+
@from.copy_to(@to).should == @to
|
196
|
+
end
|
197
|
+
|
198
|
+
it "should override without deleting other files" do
|
199
|
+
@from.copy_to(@to).should == @to
|
200
|
+
@to.file('other_file').write 'other'
|
201
|
+
|
202
|
+
@from.copy_to(@to).should == @to
|
203
|
+
@to.file('other_file').read.should == 'other'
|
204
|
+
end
|
205
|
+
|
206
|
+
it "should raise error if try to copy file as dir" do
|
207
|
+
dir = @from.dir 'file'
|
208
|
+
dir.file?.should be_true
|
209
|
+
-> {dir.copy_to @to}.should raise_error(Vfs::Error)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
describe 'general copy' do
|
214
|
+
it_should_behave_like 'copy_to behavior'
|
215
|
+
|
216
|
+
before do
|
217
|
+
# prevenging usage of :efficient_dir_copy
|
218
|
+
# Vfs::Dir.dont_use_efficient_dir_copy = true
|
219
|
+
|
220
|
+
@to = test_dir['to']
|
221
|
+
end
|
222
|
+
# after do
|
223
|
+
# Vfs::Dir.dont_use_efficient_dir_copy = false
|
224
|
+
# end
|
225
|
+
end
|
226
|
+
|
227
|
+
# describe 'effective copy' do
|
228
|
+
# it_should_behave_like 'copy_to behavior'
|
229
|
+
#
|
230
|
+
# before do
|
231
|
+
# @to = test_dir['to']
|
232
|
+
# end
|
233
|
+
# end
|
234
|
+
end
|
235
|
+
|
236
|
+
describe 'moving' do
|
237
|
+
it 'move_to' do
|
238
|
+
from, to = @path.file('from'), @path.file('to')
|
239
|
+
from.should_receive(:copy_to).with(to)
|
240
|
+
from.should_receive(:destroy).with()
|
241
|
+
from.move_to to
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'should be chainable' do
|
245
|
+
from, to = @path.dir('from').create, @path.dir('to')
|
246
|
+
from.move_to(to).should == to
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
data/spec/entry_spec.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Entry' do
|
4
|
+
with_test_dir
|
5
|
+
|
6
|
+
before do
|
7
|
+
@path = test_dir['a/b/c']
|
8
|
+
end
|
9
|
+
|
10
|
+
it "name" do
|
11
|
+
@path.name.should == 'c'
|
12
|
+
end
|
13
|
+
|
14
|
+
it "string integration" do
|
15
|
+
'/'.to_entry.path.should == '/'
|
16
|
+
'a'.to_entry.path.should == "./a"
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'tmp' do
|
20
|
+
tmp = test_dir.tmp
|
21
|
+
tmp.should be_dir
|
22
|
+
tmp.destroy
|
23
|
+
|
24
|
+
tmp = nil
|
25
|
+
test_dir.tmp do |path|
|
26
|
+
tmp = path
|
27
|
+
tmp.should be_dir
|
28
|
+
end
|
29
|
+
tmp.should_not exist
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should respond to local?' do
|
33
|
+
test_dir.should respond_to(:local?)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'created_at, updated_at, size' do
|
37
|
+
file = test_dir.file('file').write 'data'
|
38
|
+
file.created_at.class.should == Time
|
39
|
+
file.updated_at.class.should == Time
|
40
|
+
file.size.should == 4
|
41
|
+
end
|
42
|
+
end
|