io 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|