FileSet 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/doc/README +62 -0
- data/doc/Specifications +28 -0
- data/lib/fileset.rb +301 -0
- metadata +62 -0
data/doc/README
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
== Fileset
|
2
|
+
=== Simple file management
|
3
|
+
|
4
|
+
Applications tend to need preferences and configuration files, which can be a bit of a pain to maintain. Fileset allows you to describe a directory structure with raw files or YAML documents, and then populate the local directories of the install target.
|
5
|
+
|
6
|
+
=== Usage
|
7
|
+
|
8
|
+
Essentially, FileSet lets you do this:
|
9
|
+
|
10
|
+
==== Search paths
|
11
|
+
|
12
|
+
set = FileSet.new([""] + %w{etc myapp}, %w{~ .myapp}, %{.myapp})
|
13
|
+
|
14
|
+
When you create a fileset, the directories you give it define where in the
|
15
|
+
filesystem it will work.
|
16
|
+
|
17
|
+
Throughout, FileSet uses arrays of strings to reference files, which is a little cheaper when manipulating file paths, and slightly more platform agnostic than strings delimited by '/'. Development so far has been entirely in Linux, so I'd imagine this will work pretty well on OS X, and mostly all right on Windows - although I wouldn't expect Windows-style %ESCAPES% to work.
|
18
|
+
|
19
|
+
==== Definition
|
20
|
+
set.define do
|
21
|
+
dir 'www' do
|
22
|
+
file 'index.html', align(<<-EOF)
|
23
|
+
<<<
|
24
|
+
<html><body>
|
25
|
+
<h1 class="ironic">Sparse Docco</h1>
|
26
|
+
</body></html>
|
27
|
+
EOF
|
28
|
+
end
|
29
|
+
|
30
|
+
dir 'conf' do
|
31
|
+
yaml_file 'uses.yaml', {'count' => 1001}
|
32
|
+
end
|
33
|
+
|
34
|
+
dir 'empty'
|
35
|
+
end
|
36
|
+
|
37
|
+
A few features:
|
38
|
+
* a simple DSL for definition of files
|
39
|
+
* Basic text or yaml file definition
|
40
|
+
* Left flush alignment of file text (aligned an optional '<<<')
|
41
|
+
* Definition blocks can be repeated, as can dir blocks, so the FileSet can be handed around to different program modules to collect their file requirements
|
42
|
+
|
43
|
+
==== Filesystem population
|
44
|
+
set.populate
|
45
|
+
|
46
|
+
This step creates directories and writes files. The first directory listed in the search path used to initialize the FileSet that can be written to is the destination for the files. The intention is that an administrator will be able to deploy system-wide configuration, while users will still be able to install default configs in their home directories.
|
47
|
+
|
48
|
+
FileSet::populate won't overwrite existing files, so re-populating won't wipe out the configuration changes your users have made.
|
49
|
+
|
50
|
+
FileSet::populate is intentionally left as a separate step, so that it can be initiated as appropriate to the application. Some apps may want to populate automatically every time they're run, others might want to wait for a commandline switch.
|
51
|
+
|
52
|
+
If populate is never run, the default values in the define block will be returned if the files are read.
|
53
|
+
|
54
|
+
==== File access
|
55
|
+
set.load(%w{www index.html}) #=> "<html><body>...."
|
56
|
+
conf = set.get_file(%w{conf uses.yaml})
|
57
|
+
conf.contents['count'] += 1
|
58
|
+
conf.store
|
59
|
+
|
60
|
+
FileSet#load returns the contents of the file: either a string or the data stored in the YAML document.
|
61
|
+
|
62
|
+
FileSet#get_file returns a wrapper that allows the contents of the file to be accessed, changed, and then rewritten with FileSet#store.
|
data/doc/Specifications
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
FileSet retrieving files
|
3
|
+
- should get default text files
|
4
|
+
- should get text files from library files
|
5
|
+
- should get default nested files
|
6
|
+
- should get text files from filesystem
|
7
|
+
- should get yaml files named with strings
|
8
|
+
- should get yaml files from the filesystem
|
9
|
+
- should get yaml files named with symbols
|
10
|
+
|
11
|
+
FileSet defined with the DSL
|
12
|
+
- should create files on populate
|
13
|
+
- should not clobber existing files in populate
|
14
|
+
- should allow empty directory definition
|
15
|
+
- should get files once populated
|
16
|
+
|
17
|
+
FileSet defined programmatically
|
18
|
+
- should create files on populate
|
19
|
+
- should not clobber existing files in populate
|
20
|
+
- should allow empty directory definition
|
21
|
+
- should get files once populated
|
22
|
+
|
23
|
+
FileSet - the unpath method:
|
24
|
+
- state => ["state"]
|
25
|
+
|
26
|
+
Finished in 0.156611 seconds
|
27
|
+
|
28
|
+
16 examples, 0 failures
|
data/lib/fileset.rb
ADDED
@@ -0,0 +1,301 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module FileSetWorks
|
4
|
+
class LiveFile
|
5
|
+
def initialize(path, filerep)
|
6
|
+
@path = path
|
7
|
+
@rep = filerep
|
8
|
+
end
|
9
|
+
|
10
|
+
def load
|
11
|
+
::File::open(@path, "r") do |file|
|
12
|
+
@rep.load(file)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def store
|
17
|
+
::File::open(@path, "w") do |file|
|
18
|
+
@rep.store(file)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def contents
|
23
|
+
return @rep.contents
|
24
|
+
end
|
25
|
+
|
26
|
+
def contents=(new_contents)
|
27
|
+
return @rep.contents=(new_contents)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module Population
|
32
|
+
class EveryPath
|
33
|
+
def initialize(file_rep, search_paths)
|
34
|
+
@rep = file_rep
|
35
|
+
@search_paths = search_paths
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def populate
|
40
|
+
catch :done do
|
41
|
+
paths do |sp|
|
42
|
+
begin
|
43
|
+
per_path(sp)
|
44
|
+
rescue SystemCallError
|
45
|
+
next
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def paths
|
52
|
+
@search_paths.each do |sp|
|
53
|
+
yield sp
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def per_path(search_path)
|
58
|
+
@rep.create_in(search_path)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class LowestPath < EveryPath
|
63
|
+
def per_path(search_path)
|
64
|
+
@rep.create_in(search_path)
|
65
|
+
throw :done
|
66
|
+
end
|
67
|
+
|
68
|
+
def paths
|
69
|
+
@search_paths.reverse.each do |sp|
|
70
|
+
yield sp
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class HighestPath < EveryPath
|
76
|
+
def per_path(search_path)
|
77
|
+
@rep.create_in(search_path)
|
78
|
+
throw :done
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class FilesetItem
|
84
|
+
def initialize(path, populator = default_populator())
|
85
|
+
@path = path
|
86
|
+
@populator = populator
|
87
|
+
end
|
88
|
+
|
89
|
+
def default_populator
|
90
|
+
Population::LowestPath
|
91
|
+
end
|
92
|
+
|
93
|
+
def populate(search_paths)
|
94
|
+
@populator.new(self, search_paths).populate
|
95
|
+
end
|
96
|
+
|
97
|
+
def create_in(search_path)
|
98
|
+
path = ::File::join(search_path, @path)
|
99
|
+
return if ::File::exists?(path)
|
100
|
+
init_self(path)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class File < FilesetItem
|
105
|
+
def initialize(path, data, populator = default_populator())
|
106
|
+
super(path, populator)
|
107
|
+
@contents = data
|
108
|
+
end
|
109
|
+
|
110
|
+
attr_reader :path
|
111
|
+
attr_accessor :contents
|
112
|
+
|
113
|
+
def load(file)
|
114
|
+
@contents = file.read
|
115
|
+
return @contents
|
116
|
+
end
|
117
|
+
|
118
|
+
def store(file)
|
119
|
+
file.write(@contents)
|
120
|
+
end
|
121
|
+
|
122
|
+
def init_self(path)
|
123
|
+
::File::open(path, "w") do |file|
|
124
|
+
store(file)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class YAMLFile < File
|
130
|
+
#TODO: YAML::Stream
|
131
|
+
def load(file)
|
132
|
+
@contents = YAML::load(file)
|
133
|
+
return @contents
|
134
|
+
end
|
135
|
+
|
136
|
+
def store(file)
|
137
|
+
YAML::dump(@contents, file)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class Directory < FilesetItem
|
142
|
+
def default_populator
|
143
|
+
Population::EveryPath
|
144
|
+
end
|
145
|
+
|
146
|
+
def init_self(path)
|
147
|
+
FileUtils::mkdir(path)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
class FileSet
|
153
|
+
def initialize(search_paths)
|
154
|
+
@prefix = []
|
155
|
+
@files = {}
|
156
|
+
@search_paths = search_paths.map{|p| unpath([*p])}
|
157
|
+
end
|
158
|
+
|
159
|
+
def find(path)
|
160
|
+
path = unpath(path)
|
161
|
+
@search_paths.reverse.map do |sp|
|
162
|
+
::File::join(sp + path)
|
163
|
+
end.find do |path|
|
164
|
+
::File::exists?(path)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def load(*path)
|
169
|
+
get_file(path).contents
|
170
|
+
end
|
171
|
+
|
172
|
+
def get_file(path)
|
173
|
+
path = unpath(path)
|
174
|
+
file_rep = @files[path]
|
175
|
+
unless (file_path = find(path)).nil?
|
176
|
+
file = FileSetWorks::LiveFile.new(file_path, file_rep)
|
177
|
+
file.load
|
178
|
+
return file
|
179
|
+
end
|
180
|
+
return file_rep
|
181
|
+
end
|
182
|
+
|
183
|
+
def populate()
|
184
|
+
@files.keys.sort do |left, right|
|
185
|
+
left.length <=> right.length
|
186
|
+
end.each do |key|
|
187
|
+
@files[key].populate(@search_paths)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def unpath(parts)
|
192
|
+
if Array === parts and parts.length == 1
|
193
|
+
parts = parts[0]
|
194
|
+
end
|
195
|
+
|
196
|
+
case parts
|
197
|
+
when Array
|
198
|
+
if (parts.find{|part| not (String === part or Symbol === part)}.nil?)
|
199
|
+
parts = parts.map{|part| part.to_s}
|
200
|
+
else
|
201
|
+
raise "path must be composed of strings or symbols"
|
202
|
+
end
|
203
|
+
when String
|
204
|
+
parts = path_split(parts)
|
205
|
+
when Symbol
|
206
|
+
parts = [parts.to_s]
|
207
|
+
when ::File
|
208
|
+
parts = parts.path
|
209
|
+
parts = path_split(parts)
|
210
|
+
else
|
211
|
+
raise "path must be String, Array of Strings or File"
|
212
|
+
end
|
213
|
+
|
214
|
+
parts = parts.map do |part|
|
215
|
+
if /^~/ =~ part
|
216
|
+
::File::expand_path(part).split(::File::Separator)
|
217
|
+
else
|
218
|
+
part
|
219
|
+
end
|
220
|
+
end.flatten
|
221
|
+
|
222
|
+
return parts
|
223
|
+
end
|
224
|
+
|
225
|
+
def define(&block)
|
226
|
+
self.instance_eval &block
|
227
|
+
end
|
228
|
+
|
229
|
+
def add_item(path, item)
|
230
|
+
if path.length > 1
|
231
|
+
case @files[path[0..-2]]
|
232
|
+
when FileSetWorks::Directory
|
233
|
+
when nil
|
234
|
+
add_dir(path[0..-2])
|
235
|
+
else
|
236
|
+
raise "Tried to add items below #{path[0..-2]} which is not a directory"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
@files[path] = item
|
240
|
+
end
|
241
|
+
|
242
|
+
def add_dir(path)
|
243
|
+
add_item(path, FileSetWorks::Directory.new(path))
|
244
|
+
end
|
245
|
+
|
246
|
+
def add_file(path, data = nil)
|
247
|
+
add_item(path, FileSetWorks::File.new(path, data))
|
248
|
+
end
|
249
|
+
|
250
|
+
def dir(name)
|
251
|
+
path = @prefix + [name]
|
252
|
+
add_dir(path)
|
253
|
+
@prefix.push(name)
|
254
|
+
yield if block_given?
|
255
|
+
@prefix.pop
|
256
|
+
end
|
257
|
+
|
258
|
+
def file(name, data=nil)
|
259
|
+
path = @prefix + [name.to_s]
|
260
|
+
add_file(path, data)
|
261
|
+
end
|
262
|
+
|
263
|
+
def align(string)
|
264
|
+
lines = string.split(/\n/)
|
265
|
+
first = lines.shift
|
266
|
+
match = /^(\s*)<<</.match(first)
|
267
|
+
unless(match.nil?)
|
268
|
+
catch(:haircut) do
|
269
|
+
return lines.map do |line|
|
270
|
+
raise line if /^#{match[1]}|^\s*$/ !~ line
|
271
|
+
throw :haircut if /^#{match[1]}|^\s*$/ !~ line
|
272
|
+
line.sub(/^#{match[1]}/, "")
|
273
|
+
end.join("\n")
|
274
|
+
end
|
275
|
+
end
|
276
|
+
return string
|
277
|
+
end
|
278
|
+
|
279
|
+
def yaml_file(name)
|
280
|
+
path = @prefix + [name.to_s]
|
281
|
+
@files[path] = FileSetWorks::YAMLFile::new(path, yield )
|
282
|
+
end
|
283
|
+
|
284
|
+
private
|
285
|
+
|
286
|
+
def path_split(path)
|
287
|
+
path.split(::File::Separator)
|
288
|
+
end
|
289
|
+
|
290
|
+
def contents_from(*path)
|
291
|
+
path = unpath(path)
|
292
|
+
m = /(.*):\d+/.match(caller[0])
|
293
|
+
dir = ::File::dirname(::File::expand_path(m[1]))
|
294
|
+
|
295
|
+
file_path = ::File::join(unpath(dir), path)
|
296
|
+
::File::open(file_path, "r") do |file|
|
297
|
+
file.read
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: FileSet
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.1"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Judson Lester
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-29 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: " FileSet provides an API for accessing configuration and data files for your\n application, including the population of default values, and managing search\n paths. Written to encourage a cross-platform approach to maintaining configs\n for an application.\n"
|
17
|
+
email: nyarly@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- doc/README
|
24
|
+
- doc/Specifications
|
25
|
+
files:
|
26
|
+
- lib/fileset.rb
|
27
|
+
- doc/README
|
28
|
+
- doc/Specifications
|
29
|
+
has_rdoc: true
|
30
|
+
homepage: http://fileset.rubyforge.org/
|
31
|
+
licenses: []
|
32
|
+
|
33
|
+
post_install_message: Another tidy package brought to you by Judson
|
34
|
+
rdoc_options:
|
35
|
+
- --inline-source
|
36
|
+
- --main
|
37
|
+
- doc/README
|
38
|
+
- --title
|
39
|
+
- FileSet-0.1 RDoc
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
version:
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
rubyforge_project: fileset
|
57
|
+
rubygems_version: 1.3.5
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: Manage configuration and data files simply
|
61
|
+
test_files: []
|
62
|
+
|