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
@@ -0,0 +1,147 @@
|
|
1
|
+
module Vfs
|
2
|
+
class Entry
|
3
|
+
attr_reader :driver, :path, :path_cache
|
4
|
+
|
5
|
+
def initialize *args
|
6
|
+
if args.size == 1 and args.first.is_a? Entry
|
7
|
+
entry = args.first
|
8
|
+
@path_cache = entry.path_cache
|
9
|
+
@driver, @path = entry.driver, entry.path
|
10
|
+
else
|
11
|
+
driver, path = *args
|
12
|
+
@path_cache = Path.new path
|
13
|
+
@driver, @path = driver, path_cache.to_s
|
14
|
+
end
|
15
|
+
raise "driver not defined!" unless self.driver
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Navigation
|
20
|
+
#
|
21
|
+
def parent
|
22
|
+
Dir.new(driver, path_cache + '..')
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
#
|
27
|
+
# Transformations
|
28
|
+
#
|
29
|
+
def dir path = nil
|
30
|
+
if path
|
31
|
+
new_path = path_cache + path
|
32
|
+
Dir.new driver, new_path
|
33
|
+
else
|
34
|
+
Dir.new self
|
35
|
+
end
|
36
|
+
end
|
37
|
+
alias_method :to_dir, :dir
|
38
|
+
|
39
|
+
def file path = nil
|
40
|
+
if path
|
41
|
+
new_path = path_cache + path
|
42
|
+
File.new driver, new_path
|
43
|
+
else
|
44
|
+
File.new self
|
45
|
+
end
|
46
|
+
end
|
47
|
+
alias_method :to_file, :file
|
48
|
+
|
49
|
+
def entry path = nil
|
50
|
+
entry = if path
|
51
|
+
new_path = path_cache + path
|
52
|
+
klass = new_path.probably_dir? ? Dir : UniversalEntry
|
53
|
+
klass.new driver, new_path
|
54
|
+
else
|
55
|
+
UniversalEntry.new self
|
56
|
+
end
|
57
|
+
EntryProxy.new entry
|
58
|
+
end
|
59
|
+
alias_method :to_entry, :entry
|
60
|
+
|
61
|
+
|
62
|
+
#
|
63
|
+
# Attributes
|
64
|
+
#
|
65
|
+
def get attr_name = nil
|
66
|
+
attrs = driver.open{driver.attributes(path)}
|
67
|
+
(attr_name and attrs) ? attrs[attr_name] : attrs
|
68
|
+
end
|
69
|
+
|
70
|
+
def set options
|
71
|
+
# TODO2 set attributes
|
72
|
+
not_implemented
|
73
|
+
end
|
74
|
+
|
75
|
+
def dir?; !!get(:dir) end
|
76
|
+
def file?; !!get(:file) end
|
77
|
+
def created_at; get :created_at end
|
78
|
+
def updated_at; get :updated_at end
|
79
|
+
|
80
|
+
|
81
|
+
#
|
82
|
+
# Miscellaneous
|
83
|
+
#
|
84
|
+
def name
|
85
|
+
path_cache.name
|
86
|
+
end
|
87
|
+
|
88
|
+
def tmp &block
|
89
|
+
driver.open do
|
90
|
+
if block
|
91
|
+
driver.tmp do |path|
|
92
|
+
block.call Dir.new(driver, path)
|
93
|
+
end
|
94
|
+
else
|
95
|
+
Dir.new driver, driver.tmp
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def local?
|
101
|
+
driver.local?
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
#
|
106
|
+
# Utils
|
107
|
+
#
|
108
|
+
def inspect
|
109
|
+
"#{driver}#{':' unless driver.to_s.empty?}#{path}"
|
110
|
+
end
|
111
|
+
alias_method :to_s, :inspect
|
112
|
+
|
113
|
+
def == other
|
114
|
+
return false unless other.is_a? Entry
|
115
|
+
driver == other.driver and path == other.path
|
116
|
+
end
|
117
|
+
|
118
|
+
def hash
|
119
|
+
driver.hash + path.hash
|
120
|
+
end
|
121
|
+
|
122
|
+
def eql? other
|
123
|
+
return false unless other.class == self.class
|
124
|
+
driver.eql?(other.driver) and path.eql?(other.path)
|
125
|
+
end
|
126
|
+
|
127
|
+
protected
|
128
|
+
def destroy_entry first = :file, second = :dir
|
129
|
+
driver.open do
|
130
|
+
begin
|
131
|
+
driver.send :"delete_#{first}", path
|
132
|
+
rescue StandardError => e
|
133
|
+
attrs = get
|
134
|
+
if attrs and attrs[first]
|
135
|
+
# some unknown error
|
136
|
+
raise e
|
137
|
+
elsif attrs and attrs[second]
|
138
|
+
driver.send :"delete_#{second}", path
|
139
|
+
else
|
140
|
+
# do nothing, entry already not exist
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
self
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module Vfs
|
2
|
+
class File < Entry
|
3
|
+
#
|
4
|
+
# Attributes
|
5
|
+
#
|
6
|
+
alias_method :exist?, :file?
|
7
|
+
|
8
|
+
|
9
|
+
#
|
10
|
+
# CRUD
|
11
|
+
#
|
12
|
+
def read options = {}, &block
|
13
|
+
options[:bang] = true unless options.include? :bang
|
14
|
+
driver.open do
|
15
|
+
begin
|
16
|
+
if block
|
17
|
+
driver.read_file path, &block
|
18
|
+
else
|
19
|
+
data = ""
|
20
|
+
driver.read_file(path){|buff| data << buff}
|
21
|
+
data
|
22
|
+
end
|
23
|
+
rescue StandardError => e
|
24
|
+
raise Vfs::Error, "can't read Dir #{self}!" if dir.exist?
|
25
|
+
attrs = get
|
26
|
+
if attrs and attrs[:file]
|
27
|
+
# unknown internal error
|
28
|
+
raise e
|
29
|
+
elsif attrs and attrs[:dir]
|
30
|
+
raise Error, "You are trying to read Dir '#{self}' as if it's a File!"
|
31
|
+
else
|
32
|
+
if options[:bang]
|
33
|
+
raise Error, "file #{self} not exist!"
|
34
|
+
else
|
35
|
+
block ? block.call('') : ''
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# def content options = {}
|
43
|
+
# read options
|
44
|
+
# end
|
45
|
+
|
46
|
+
def create options = {}
|
47
|
+
write '', options
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def write *args, &block
|
52
|
+
if block
|
53
|
+
options = args.first || {}
|
54
|
+
else
|
55
|
+
data, options = *args
|
56
|
+
options ||= {}
|
57
|
+
end
|
58
|
+
raise "can't do :override and :append at the same time!" if options[:override] and options[:append]
|
59
|
+
|
60
|
+
driver.open do
|
61
|
+
try = 0
|
62
|
+
begin
|
63
|
+
try += 1
|
64
|
+
if block
|
65
|
+
driver.write_file(path, options[:append], &block)
|
66
|
+
else
|
67
|
+
driver.write_file(path, options[:append]){|writer| writer.write data}
|
68
|
+
end
|
69
|
+
rescue StandardError => error
|
70
|
+
parent = self.parent
|
71
|
+
if entry.exist?
|
72
|
+
entry.destroy
|
73
|
+
elsif !parent.exist?
|
74
|
+
parent.create(options)
|
75
|
+
else
|
76
|
+
# unknown error
|
77
|
+
raise error
|
78
|
+
end
|
79
|
+
|
80
|
+
try < 2 ? retry : raise(error)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
def append *args, &block
|
87
|
+
options = (args.last.is_a?(Hash) && args.pop) || {}
|
88
|
+
options[:append] = true
|
89
|
+
write(*(args << options), &block)
|
90
|
+
end
|
91
|
+
|
92
|
+
def update options = {}, &block
|
93
|
+
data = read options
|
94
|
+
write block.call(data), options
|
95
|
+
end
|
96
|
+
|
97
|
+
def destroy
|
98
|
+
destroy_entry
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
#
|
103
|
+
# Transfers
|
104
|
+
#
|
105
|
+
def copy_to to, options = {}
|
106
|
+
raise Error, "you can't copy to itself" if self == to
|
107
|
+
|
108
|
+
target = if to.is_a? File
|
109
|
+
to
|
110
|
+
elsif to.is_a? Dir
|
111
|
+
to.file #(name)
|
112
|
+
elsif to.is_a? UniversalEntry
|
113
|
+
to.file
|
114
|
+
else
|
115
|
+
raise "can't copy to unknown Entry!"
|
116
|
+
end
|
117
|
+
|
118
|
+
target.write options do |writer|
|
119
|
+
read(options){|buff| writer.write buff}
|
120
|
+
end
|
121
|
+
|
122
|
+
target
|
123
|
+
end
|
124
|
+
|
125
|
+
def move_to to
|
126
|
+
copy_to to
|
127
|
+
destroy
|
128
|
+
to
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
#
|
133
|
+
# Extra Stuff
|
134
|
+
#
|
135
|
+
def render *args
|
136
|
+
require 'tilt'
|
137
|
+
|
138
|
+
args.unshift Object.new if args.size == 1 and args.first.is_a?(Hash)
|
139
|
+
|
140
|
+
template = Tilt.new(path){read}
|
141
|
+
template.render *args
|
142
|
+
end
|
143
|
+
|
144
|
+
def size; get :size end
|
145
|
+
|
146
|
+
def basename
|
147
|
+
::File.basename(name, File.extname(name))
|
148
|
+
end
|
149
|
+
|
150
|
+
def extension
|
151
|
+
::File.extname(name).sub(/^\./, '')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Vfs
|
2
|
+
class UniversalEntry < Entry
|
3
|
+
#
|
4
|
+
# Attributes
|
5
|
+
#
|
6
|
+
def exist?
|
7
|
+
!!get
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def copy_to to, options = {}
|
12
|
+
from = file? ? to_file : to_dir
|
13
|
+
from.copy_to to, options
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
#
|
18
|
+
# CRUD
|
19
|
+
#
|
20
|
+
def destroy
|
21
|
+
destroy_entry
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#
|
2
|
+
# It allows dynamically (magically) switching between UniversalEntry/Dir/File
|
3
|
+
#
|
4
|
+
module Vfs
|
5
|
+
class EntryProxy < BasicObject
|
6
|
+
attr_reader :_target
|
7
|
+
|
8
|
+
def initialize entry
|
9
|
+
raise 'something wrong happening here!' if entry.respond_to?(:proxy?) and entry.proxy?
|
10
|
+
self._target = entry
|
11
|
+
end
|
12
|
+
|
13
|
+
def proxy?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
protected :==, :equal?, :!, :!=
|
18
|
+
protected
|
19
|
+
attr_writer :_target
|
20
|
+
|
21
|
+
def respond_to? m
|
22
|
+
super or
|
23
|
+
::Vfs::UniversalEntry.method_defined?(m) or
|
24
|
+
::Vfs::Dir.method_defined?(m) or
|
25
|
+
::Vfs::File.method_defined?(m)
|
26
|
+
end
|
27
|
+
|
28
|
+
def method_missing m, *a, &b
|
29
|
+
unless _target.respond_to? m
|
30
|
+
if ::Vfs::UniversalEntry.method_defined? m
|
31
|
+
self.target = _target.entry
|
32
|
+
elsif ::Vfs::Dir.method_defined? m
|
33
|
+
self._target = _target.dir
|
34
|
+
elsif ::Vfs::File.method_defined? m
|
35
|
+
self._target = _target.file
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
_target.send m, *a, &b
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/vfs/error.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
class String
|
2
|
+
def to_entry_on driver = nil
|
3
|
+
path = self
|
4
|
+
driver ||= Vfs.default_driver
|
5
|
+
|
6
|
+
path = "./#{path}" unless path =~ /^[\/\.\~]/
|
7
|
+
Vfs::Entry.new(driver, path).entry
|
8
|
+
end
|
9
|
+
alias_method :to_entry, :to_entry_on
|
10
|
+
|
11
|
+
def to_file_on driver = nil
|
12
|
+
to_entry_on(driver).file
|
13
|
+
end
|
14
|
+
alias_method :to_file, :to_file_on
|
15
|
+
|
16
|
+
def to_dir_on driver = nil
|
17
|
+
to_entry_on(driver).dir
|
18
|
+
end
|
19
|
+
alias_method :to_dir, :to_dir_on
|
20
|
+
end
|
21
|
+
|
22
|
+
class File
|
23
|
+
def to_entry
|
24
|
+
path.to_entry
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_file
|
28
|
+
path.to_file
|
29
|
+
end
|
30
|
+
end
|
data/lib/vfs/path.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
module Vfs
|
2
|
+
class Path < String
|
3
|
+
def initialize path = '/', options = {}
|
4
|
+
if options[:skip_normalization]
|
5
|
+
super path
|
6
|
+
@probably_dir = options[:probably_dir]
|
7
|
+
else
|
8
|
+
Path.validate! path
|
9
|
+
path, probably_dir = Path.normalize_to_string path
|
10
|
+
raise "invalid path '#{path}' (you are outside of the root)!" unless path
|
11
|
+
super path
|
12
|
+
@probably_dir = probably_dir
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def + path = ''
|
17
|
+
path = path.to_s
|
18
|
+
Path.validate! path, false
|
19
|
+
|
20
|
+
if Path.absolute?(path)
|
21
|
+
Path.normalize path
|
22
|
+
elsif path.empty?
|
23
|
+
self
|
24
|
+
else
|
25
|
+
Path.normalize "#{self}#{'/' unless self == '/'}#{path}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def parent
|
30
|
+
self + '..'
|
31
|
+
end
|
32
|
+
|
33
|
+
def probably_dir?
|
34
|
+
!!@probably_dir
|
35
|
+
end
|
36
|
+
|
37
|
+
def name
|
38
|
+
unless @name
|
39
|
+
root = self[0..0]
|
40
|
+
@name ||= split('/').last || root
|
41
|
+
end
|
42
|
+
@name
|
43
|
+
end
|
44
|
+
|
45
|
+
class << self
|
46
|
+
def absolute? path
|
47
|
+
path =~ /^[\/~\/]|^\.$|^\.\//
|
48
|
+
end
|
49
|
+
|
50
|
+
def valid? path, forbid_relative = true, &block
|
51
|
+
result, err = if forbid_relative and !absolute?(path)
|
52
|
+
[false, "path must be started with '/', or '.'"]
|
53
|
+
elsif path =~ /.+\/~$|.+\/$|\/\.$/
|
54
|
+
[false, "path can't be ended with '/', '/~', or '/.'"]
|
55
|
+
elsif path =~ /\/\/|\/~\/|\/\.\//
|
56
|
+
[false, "path can't include '/./', '/~/', '//' combinations!"]
|
57
|
+
# elsif path =~ /.+[~]|\/\.\//
|
58
|
+
# [false, "'~', or '.' can be present only at the begining of string"]
|
59
|
+
else
|
60
|
+
[true, nil]
|
61
|
+
end
|
62
|
+
|
63
|
+
block.call err if block and !result and err
|
64
|
+
result
|
65
|
+
end
|
66
|
+
|
67
|
+
def normalize path
|
68
|
+
path, probably_dir = normalize_to_string path
|
69
|
+
unless path
|
70
|
+
nil
|
71
|
+
else
|
72
|
+
Path.new(path, skip_normalization: true, probably_dir: probably_dir)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def validate! path, forbid_relative = true
|
77
|
+
valid?(path, forbid_relative){|error| raise "invalid path '#{path}' (#{error})!"}
|
78
|
+
end
|
79
|
+
|
80
|
+
def normalize_to_string path
|
81
|
+
root = path[0..0]
|
82
|
+
result, probably_dir = [], false
|
83
|
+
|
84
|
+
parts = path.split('/')[1..-1]
|
85
|
+
if parts
|
86
|
+
parts.each do |part|
|
87
|
+
if part == '..' and root != '.'
|
88
|
+
return nil, false unless result.size > 0
|
89
|
+
result.pop
|
90
|
+
probably_dir ||= true
|
91
|
+
# elsif part == '.'
|
92
|
+
# # do nothing
|
93
|
+
else
|
94
|
+
result << part
|
95
|
+
probably_dir &&= false
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
normalized_path = result.join('/')
|
100
|
+
|
101
|
+
probably_dir ||= true if normalized_path.empty?
|
102
|
+
|
103
|
+
return "#{root}#{'/' unless root == '/' or normalized_path.empty?}#{normalized_path}", probably_dir
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# protected
|
108
|
+
# def delete_dir_mark
|
109
|
+
# path = path.to_s.sub(%r{/$}, '')
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
#
|
113
|
+
# def root_path? path
|
114
|
+
# path =~ /^[#{ROOT_SYMBOLS}]$/
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# def split_path path
|
118
|
+
# path.split(/#{ROOT_SYMBOLS}/)
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# def dir_mark? path
|
122
|
+
# path =~ %r{/$}
|
123
|
+
# end
|
124
|
+
end
|
125
|
+
end
|