vfs 0.0.4 → 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.
Files changed (44) hide show
  1. data/Rakefile +2 -2
  2. data/lib/vfs.rb +18 -0
  3. data/lib/vfs/entries/dir.rb +272 -0
  4. data/lib/vfs/entries/entry.rb +127 -0
  5. data/lib/vfs/entries/file.rb +185 -0
  6. data/lib/vfs/entries/universal_entry.rb +24 -0
  7. data/lib/vfs/entry_proxy.rb +38 -0
  8. data/lib/vfs/error.rb +4 -0
  9. data/lib/vfs/integration/string.rb +19 -0
  10. data/lib/vfs/path.rb +125 -0
  11. data/lib/vfs/storages/hash_fs.rb +194 -0
  12. data/lib/vfs/storages/local.rb +138 -0
  13. data/lib/vfs/storages/specification.rb +127 -0
  14. data/lib/vfs/support.rb +2 -0
  15. data/readme.md +110 -14
  16. data/spec/container_spec.rb +31 -0
  17. data/spec/dir_spec.rb +224 -0
  18. data/spec/entry_spec.rb +24 -0
  19. data/spec/file_spec.rb +189 -0
  20. data/spec/path_spec.rb +121 -0
  21. data/spec/spec_helper.rb +2 -9
  22. data/spec/storages/hash_fs_spec.rb +10 -0
  23. data/spec/storages/local_spec.rb +10 -0
  24. data/spec/universal_entry_spec.rb +42 -0
  25. metadata +25 -23
  26. data/lib/old/ssh.rb +0 -11
  27. data/lib/rsh.rb +0 -19
  28. data/lib/rsh/box.rb +0 -182
  29. data/lib/rsh/box/marks.rb +0 -29
  30. data/lib/rsh/drivers/abstract.rb +0 -15
  31. data/lib/rsh/drivers/local.rb +0 -48
  32. data/lib/rsh/drivers/ssh.rb +0 -147
  33. data/lib/rsh/gems.rb +0 -2
  34. data/lib/rsh/support.rb +0 -30
  35. data/spec/abstract_driver.rb +0 -82
  36. data/spec/abstract_driver/dir/dir2/file +0 -0
  37. data/spec/abstract_driver/local_file +0 -1
  38. data/spec/box_spec.rb +0 -109
  39. data/spec/box_spec/dir/dir2/file +0 -0
  40. data/spec/box_spec/local_file +0 -1
  41. data/spec/config.example.yml +0 -4
  42. data/spec/config.yml +0 -5
  43. data/spec/local_driver_spec.rb +0 -9
  44. data/spec/ssh_driver_spec.rb +0 -15
@@ -0,0 +1,127 @@
1
+ # use '$ gem install ruby_ext' to install.
2
+ require 'rspec_ext'
3
+ require 'ruby_ext'
4
+
5
+ shared_examples_for 'vfs storage' do
6
+ before :each do
7
+ @storage.open_fs do |fs|
8
+ @tmp_dir = fs.tmp
9
+ end
10
+ end
11
+
12
+ after :each do
13
+ @storage.open_fs do |fs|
14
+ attrs = fs.attributes(@tmp_dir)
15
+ fs.delete_dir @tmp_dir if attrs && attrs[:dir]
16
+ end
17
+ end
18
+
19
+ it 'should respond to :local?' do
20
+ @storage.open_fs{|fs| fs.should respond_to(:local?)}
21
+ end
22
+
23
+ it 'should have root dir' do
24
+ @storage.open_fs do |fs|
25
+ fs.attributes('/').subset(:file, :dir).should == {file: false, dir: true}
26
+ end
27
+ end
28
+
29
+ describe "files" do
30
+ before :each do
31
+ @remote_file = "#{@tmp_dir}/remote_file"
32
+ end
33
+
34
+ it "file attributes" do
35
+ @storage.open_fs do |fs|
36
+ fs.attributes(@remote_file).should == {}
37
+ fs.write_file(@remote_file, false){|w| w.call 'something'}
38
+ attrs = fs.attributes(@remote_file)
39
+ fs.attributes(@remote_file).subset(:file, :dir).should == {file: true, dir: false}
40
+ end
41
+ end
42
+
43
+ it "read, write & append" do
44
+ @storage.open_fs do |fs|
45
+ fs.write_file(@remote_file, false){|w| w.call 'something'}
46
+ fs.attributes(@remote_file)[:file].should be_true
47
+
48
+ data = ""
49
+ fs.read_file(@remote_file){|buff| data << buff}
50
+ data.should == 'something'
51
+
52
+ # append
53
+ fs.write_file(@remote_file, true){|w| w.call ' another'}
54
+ data = ""
55
+ fs.read_file(@remote_file){|buff| data << buff}
56
+ data.should == 'something another'
57
+ end
58
+ end
59
+
60
+ it "delete_file" do
61
+ @storage.open_fs do |fs|
62
+ fs.write_file(@remote_file, false){|w| w.call 'something'}
63
+ fs.attributes(@remote_file)[:file].should be_true
64
+ fs.delete_file(@remote_file)
65
+ fs.attributes(@remote_file).should == {}
66
+ end
67
+ end
68
+ end
69
+
70
+ describe 'directories' do
71
+ # before :each do
72
+ # @from_local, @remote_path, @to_local = "#{@local_dir}/dir", "#{@tmp_dir}/upload", "#{@local_dir}/download"
73
+ # end
74
+
75
+ before :each do
76
+ @remote_dir = "#{@tmp_dir}/some_dir"
77
+ end
78
+
79
+ it "directory_exist?, create_dir, delete_dir" do
80
+ @storage.open_fs do |fs|
81
+ fs.attributes(@remote_dir).should == {}
82
+ fs.create_dir(@remote_dir)
83
+ fs.attributes(@remote_dir).subset(:file, :dir).should == {file: false, dir: true}
84
+ fs.delete_dir(@remote_dir)
85
+ fs.attributes(@remote_dir).should == {}
86
+ end
87
+ end
88
+
89
+ it 'should delete not-empty directories' do
90
+ @storage.open_fs do |fs|
91
+ fs.create_dir(@remote_dir)
92
+ fs.create_dir("#{@remote_dir}/dir")
93
+ fs.write_file("#{@remote_dir}/dir/file", false){|w| w.call 'something'}
94
+ fs.delete_dir(@remote_dir)
95
+ fs.attributes(@remote_dir).should == {}
96
+ end
97
+ end
98
+
99
+ it 'each' do
100
+ @storage.open_fs do |fs|
101
+ list = {}
102
+ fs.each(@tmp_dir){|path, type| list[path] = type}
103
+ list.should be_empty
104
+
105
+ dir, file = "#{@tmp_dir}/dir", "#{@tmp_dir}/file"
106
+ fs.create_dir(dir)
107
+ fs.write_file(file, false){|w| w.call 'something'}
108
+
109
+ list = {}
110
+ fs.each(@tmp_dir){|path, type| list[path] = type}
111
+ list.should == {'dir' => :dir, 'file' => :file}
112
+ end
113
+ end
114
+
115
+ # it "upload_directory & download_directory" do
116
+ # upload_path_check = "#{@remote_path}/dir2/file"
117
+ # check_attributes upload_path_check, nil
118
+ # fs.upload_directory(@from_local, @remote_path)
119
+ # check_attributes upload_path_check, file: true, dir: false
120
+ #
121
+ # download_path_check = "#{@to_local}/dir2/file"
122
+ # File.exist?(download_path_check).should be_false
123
+ # fs.download_directory(@remote_path, @to_local)
124
+ # File.exist?(download_path_check).should be_true
125
+ # end
126
+ end
127
+ end
@@ -0,0 +1,2 @@
1
+ require 'fileutils'
2
+ require 'set'
data/readme.md CHANGED
@@ -1,21 +1,117 @@
1
- # Rsh - Tiny wrapper over Net::SSH/SFTP
1
+ # Vfs - Virtual File System
2
2
 
3
- Because they are too hard to use and have terrible API design.
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 Storages is the same as ActiveRecord is for Relational Databases.
4
5
 
5
- box = Rsh::Box.new host: 'webapp.com', ssh: {user: 'root', password: 'secret'}
6
+ Currently, there are following implementations available:
6
7
 
7
- box.upload_directory '/my_project', '/apps/my_project'
8
- box.bash 'nohup /apps/my_project/server_start'
9
-
10
- Honestly my wrapper also not very good. I would like to make API looks like the ['rush'][rush] gem (made by Adam Wiggins)
11
- but it requires a lots of time, maybe I'll do it later.
12
- So, for now it's just a small wrapper to do ssh/io operations not so painfull.
8
+ - local file system
9
+ - remote file system (over ssh)
10
+
11
+ ## Goals
12
+
13
+ - **handy, simple and clean** API.
14
+ - **high performance** - the same as by using low-level storage API, there should be no extra calls **.
15
+ - same API for different storages (Local FS, SSH, Hadoop, or any other , ...).
16
+ - should work **simultaneously with different storages**.
17
+ - small codebase, easy to extend by others.
18
+ - simple storage-driver implementation, easy add new storage types (Hadoop DFS, LDAP, Document Oriented DB, In-Memory, ...).
19
+
20
+ ** all methods should have the same performance as native system calls, except for :move and :rename. Right now they are implemented
21
+ ASAP by using copy+destroy approach, will be fixed as soon as I'll have free time to do it.
22
+
23
+ ## Code samples:
24
+ gem 'vfs' # Virtual File System
25
+ require 'vfs'
26
+
27
+ gem 'vos' # Virtual Operating System
28
+ require 'vos'
29
+
30
+
31
+ # Connections, let's deploy our 'cool_app' project from our local box to remote server
32
+ server = Vfs::Box.new(host: 'cool_app.com', ssh: {user: 'me', password: 'secret'})
33
+ me = '~'.to_dir
34
+
35
+ cool_app = server['apps/cool_app']
36
+ projects = me['projects']
37
+
38
+
39
+ # Working with dirs, copying dir from any source to any destination (local/remote/custom_storage_type)
40
+ projects['cool_app'].copy_to cool_app
41
+
42
+
43
+ # Working with files
44
+ dbc = cool_app.file('config/database.yml') # <= the 'config' dir not exist yet
45
+ dbc.write("user: root\npassword: secret") # <= now the 'database.yml' and parent 'config' has been created
46
+ dbc.content =~ /database/ # => false, we forgot to add the database
47
+ dbc.append("\ndatabase: mysql") # let's do it
48
+
49
+ dbc.update do |content| # and add host info
50
+ content + "\nhost: cool_app.com "
51
+ end
52
+
53
+ projects['cool_app/config/database.yml']. # or just overwrite it with our local dev version
54
+ copy_to! dbc
55
+
56
+ # there are also streaming support (read/write/append), please go to specs for docs
57
+
58
+
59
+ # Checks
60
+ cool_app['config'].exist? # => true
61
+ cool_app.dir('config').exist? # => true
62
+ cool_app.file('config').exist? # => false
13
63
 
64
+ cool_app['config'].dir? # => true
65
+ cool_app['config'].file? # => false
66
+
67
+
68
+ # Navigation
69
+ config = cool_app['config']
70
+ config.parent # => </apps/cool_app>
71
+ config['../..'] # => </>
72
+ config['../..'].dir? # => true
73
+
74
+ cool_app.entries # => list of dirs and files, also support &block
75
+ cool_app.files # => list of files, also support &block
76
+ cool_app.dirs # => list of dirs, also support &block
77
+
78
+
79
+ # For more please go to specs (create/update/move/copy/destroy/...)
80
+
81
+ ## Integration with [Vos][vos] (Virtual Operating System)
82
+
83
+ server['apps/cool_app'].bash 'rails production'
84
+
85
+ For more details please go to [Vos][vos] project page.
86
+
87
+ # Why?
88
+
89
+ To easy my work: with local FS, remote FS (cluster management, deployment automation), and some specific systems like Hadoop DFS.
90
+
91
+ Because the API of standard File/Dir/FileUtils classes are just terrible. And there's the reason for it - the goal of thouse tools
92
+ is to provide 1-to-1 clone of underlying OS API, instead of provididing handy tool.
93
+
94
+ And if you want to use remote FS - things are getting even worse and more complicated (Net::SSH & Net::SFTP use a little
95
+ different API than local FS, and you has to remember all thouse little quirks).
96
+
14
97
  ## TODO
15
98
 
16
- - remove ssh.remote and use only open/close
17
- - introduce Entity/Dir/File (the same as in Rush)
18
- - allow to move files between any Boxes, not only between local and remote.
19
- - add support for moving dirs.
99
+ ### v 0.1 (all done)
100
+
101
+ - Vos: Dir.bash
102
+ - File.append
103
+ - list of entries/files/dirs
104
+ - support for efficient copy for Local and SSH storages
105
+
106
+ ### v 0.2 (not started)
107
+
108
+ - efficient (not copy/destroy) versions of move_to, rename
109
+ - glob search for directories: Dir['**/*.yml']
110
+ - access via attributes and helpers for unix chmod
111
+ - add storages: remote FS over HTTP.
112
+
113
+ ### future
114
+
115
+ - add storages: Hadoop DFS, MongoDB, Amazon S3
20
116
 
21
- [rush]: http://github.com/adamwiggins/rush
117
+ [vos]: http://github.com/alexeypetrushin/vos
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Container' do
4
+ before :each do
5
+ @fs = '/'.to_entry_on(Vfs::Storages::HashFs.new)
6
+ end
7
+
8
+ it "should threat paths as UniversalEntry except it ends with '/'" do
9
+ @fs.should_receive(:entry).with('/a/b')
10
+ @fs['/a/b']
11
+
12
+ @fs.should_receive(:dir).with('/a/b')
13
+ @fs['/a/b/']
14
+ end
15
+
16
+ it '/' do
17
+ @fs[:some_path].should == @fs / :some_path
18
+ end
19
+
20
+ it "UniversalEntry should be wrapped inside of proxy, Dir and File should not" do
21
+ -> {@fs.dir.proxy?}.should raise_error(NoMethodError)
22
+ -> {@fs.file.proxy?}.should raise_error(NoMethodError)
23
+ @fs.entry.proxy?.should be_true
24
+ end
25
+
26
+ it "sometimes it also should inexplicitly guess that path is a Dir instead of UniversalEntry (but still wrap it inside of Proxy)" do
27
+ dir = @fs['/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,224 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Dir' do
4
+ before :each do
5
+ @fs = '/'.to_entry_on(Vfs::Storages::HashFs.new)
6
+ @path = @fs['/a/b/c']
7
+ end
8
+
9
+ describe 'existence' do
10
+ it "should check only dirs" do
11
+ @path.should_not exist
12
+ @path.file.create
13
+ @path.should be_file
14
+ @path.dir.should_not exist
15
+ @path.dir.create!
16
+ @path.should be_dir
17
+ @path.dir.should exist
18
+ end
19
+ end
20
+
21
+ it "should not respond to read and write methods" do
22
+ -> {@path.dir.read}.should raise_error(NoMethodError)
23
+ -> {@path.dir.write}.should raise_error(NoMethodError)
24
+ end
25
+
26
+ describe 'create' do
27
+ it 'should be chainable' do
28
+ @path.dir.create.should == @path
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 if override specified' do
44
+ @path.file.create
45
+ @path.should be_file
46
+ -> {@path.dir.create}.should raise_error(Vfs::Error, /exist/)
47
+ @path.dir.create!
48
+ @path.should be_dir
49
+ end
50
+
51
+ it 'should override existing dir if override specified' do
52
+ @path.dir.create
53
+ @path.should be_dir
54
+ @path.dir.create!
55
+ @path.should be_dir
56
+ end
57
+ end
58
+
59
+ describe 'destroying' do
60
+ it "should raise error if it's trying to destroy a file (unless force specified)" do
61
+ @path.file.create
62
+ -> {@path.dir.destroy}.should raise_error(Vfs::Error, /can't destroy File/)
63
+ @path.dir.destroy!
64
+ @path.entry.should_not exist
65
+ end
66
+
67
+ it "shouldn't raise if dir not exist" do
68
+ @path.dir.destroy
69
+ end
70
+
71
+ it 'should destroy recursivelly' do
72
+ dir = @path.dir
73
+ dir.create
74
+ dir.file('file').write 'something'
75
+ dir.dir('dir').create.tap do |dir|
76
+ dir.file('file2').write 'something2'
77
+ end
78
+
79
+ dir.destroy
80
+ dir.should_not exist
81
+ end
82
+
83
+ it 'should be chainable' do
84
+ @path.dir.destroy.should == @path
85
+ @path.dir.destroy!.should == @path
86
+ end
87
+ end
88
+
89
+ describe 'content' do
90
+ before :each do
91
+ @path.dir('dir').create
92
+ @path.dir('dir/another_dir').create
93
+ @path.file('file').create
94
+ end
95
+
96
+ it 'entries' do
97
+ -> {@path['non_existing'].entries}.should raise_error(Vfs::Error, /not exist/)
98
+ @path['non_existing'].entries(bang: false).should == []
99
+ @path.entries.to_set.should be_eql([@path.dir('dir'), @path.file('file')].to_set)
100
+ list = []
101
+ @path.entries{|e| list << e}
102
+ list.to_set.should be_eql([@path.dir('dir'), @path.file('file')].to_set)
103
+ end
104
+
105
+ it 'should raise error if trying :entries on file' do
106
+ @path.file('some_file').create
107
+ -> {@path.dir('some_file').entries}.should raise_error(/File/)
108
+ end
109
+
110
+ it 'files' do
111
+ @path.files.should be_eql([@path.file('file')])
112
+ end
113
+
114
+ it 'dirs' do
115
+ @path.dirs.should be_eql([@path.dir('dir')])
116
+ end
117
+
118
+ it 'has? & include?' do
119
+ @path.include?('dir').should be_true
120
+ @path.include?('dir/another_dir').should be_true
121
+ @path.include?('file').should be_true
122
+ @path.include?('non_existing').should be_false
123
+ end
124
+ end
125
+
126
+ describe 'copying' do
127
+ before :each do
128
+ @from = @path.dir
129
+ @from.create
130
+ @from.file('file').write 'something'
131
+ @from.dir('dir').create.tap do |dir|
132
+ dir.file('file2').write 'something2'
133
+ end
134
+ end
135
+
136
+ it 'should not copy to itself' do
137
+ -> {@from.copy_to @from}.should raise_error(Vfs::Error, /itself/)
138
+ end
139
+
140
+ def check_copy_for to, error_re1, error_re2
141
+ begin
142
+ target = @from.copy_to to
143
+ target['file'].read.should == 'something'
144
+ target['dir/file2'].read.should == 'something2'
145
+ target.should == to
146
+ rescue Exception => e
147
+ raise e unless e.message =~ error_re1
148
+ end
149
+
150
+ @from['dir/file2'].write! 'another'
151
+ -> {@from.copy_to to}.should raise_error(Vfs::Error, error_re2)
152
+ target = @from.copy_to! to
153
+ target['file'].read.should == 'something'
154
+ target['dir/file2'].read.should == 'another'
155
+ end
156
+
157
+ describe 'general copy' do
158
+ before :each do
159
+ # we using here another HashFs storage, to prevent :effective_dir_copy to be used
160
+ @to = '/'.to_entry_on(Vfs::Storages::HashFs.new)['to']
161
+
162
+ @from.storage.should_not_receive(:for_spec_helper_effective_copy_used)
163
+ end
164
+
165
+ it 'should copy to file (and overwrite if forced)' do
166
+ check_copy_for @to.file, /can't copy Dir to File/, /can't copy Dir to File/
167
+ end
168
+
169
+ it 'should copy to dir (and overwrite if forced)' do
170
+ check_copy_for @to.dir, nil, /exist/
171
+ end
172
+
173
+ it 'should copy to UniversalEntry (and overwrite if forced)' do
174
+ check_copy_for @to.entry, nil, /exist/
175
+ end
176
+ end
177
+
178
+ describe 'effective copy' do
179
+ before :each do
180
+ # we using the same HashFs storage, so :effective_dir_copy will be used
181
+ @to = @fs['to']
182
+
183
+ @from.storage.should_receive(:for_spec_helper_effective_copy_used).at_least(1).times
184
+ end
185
+
186
+ it 'should copy to file (and overwrite if forced)' do
187
+ check_copy_for @to.file, /can't copy Dir to File/, /can't copy Dir to File/
188
+ end
189
+
190
+ it 'should copy to dir (and overwrite if forced)' do
191
+ check_copy_for @to.dir, nil, /exist/
192
+ end
193
+
194
+ it 'should copy to UniversalEntry (and overwrite if forced)' do
195
+ check_copy_for @to.entry, nil, /exist/
196
+ end
197
+ end
198
+
199
+ it 'should be chainable' do
200
+ to = @fs['to']
201
+ @from.copy_to(to).should == to
202
+ @from.copy_to!(to).should == to
203
+ end
204
+ end
205
+
206
+ describe 'moving' do
207
+ it 'move_to' do
208
+ from, to = @path.file('from'), @path.file('to')
209
+ from.should_receive(:copy_to).with(to, {})
210
+ from.should_receive(:destroy).with({})
211
+ from.move_to to
212
+
213
+ from.should_receive(:move_to).with(to, override: true)
214
+ from.move_to! to
215
+ end
216
+
217
+ it 'should be chainable' do
218
+ from, to = @path.dir('from').create, @path.dir('to')
219
+ from.move_to(to).should == to
220
+ from.create
221
+ from.move_to!(to).should == to
222
+ end
223
+ end
224
+ end