sgfa 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,203 @@
1
+ #
2
+ # Simple Group of Filing Applications
3
+ # Binder implemented using filesystem storage and locking
4
+ #
5
+ # Copyright (C) 2015 by Graham A. Field.
6
+ #
7
+ # See LICENSE.txt for licensing information.
8
+ #
9
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
10
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+
12
+ require 'json'
13
+ require 'tempfile'
14
+
15
+ require_relative 'error'
16
+ require_relative 'binder'
17
+ require_relative 'jacket_fs'
18
+ require_relative 'lock_fs'
19
+
20
+ module Sgfa
21
+
22
+
23
+ #####################################################################
24
+ # An implementation of {Binder} using file system storage, {LockFs},
25
+ # and {JacketFs}.
26
+ class BinderFs < Binder
27
+
28
+ #####################################
29
+ # Create a new binder
30
+ #
31
+ # @param path [String] Path to the binder
32
+ # @param tr (see Binder#jacket_create)
33
+ # @param init [Hash] The binder initialization options
34
+ # @return [String] The hash ID of the new binder
35
+ def create(path, tr, init)
36
+ raise Error::Sanity, 'Binder already open' if @path
37
+ Binder.limits_create(init)
38
+
39
+ # create directory
40
+ begin
41
+ Dir.mkdir(path)
42
+ rescue Errno::EEXIST
43
+ raise Error::Conflict, 'Binder path already exists'
44
+ end
45
+
46
+ # create control binder
47
+ dn_ctrl = File.join(path, '0')
48
+ id_hash = JacketFs.create(dn_ctrl, init[:id_text])
49
+ jck = JacketFs.new(dn_ctrl)
50
+
51
+ # do the common creation
52
+ @path = path
53
+ @id_hash = id_hash
54
+ @jackets = {}
55
+ @users = {}
56
+ @values = {}
57
+ _create(jck, tr, init)
58
+ jck.close
59
+ _cache_write
60
+ @path = nil
61
+ @id_hash = nil
62
+
63
+ # write info
64
+ info = {
65
+ 'sgfa_binder_ver' => 1,
66
+ 'id_hash' => id_hash,
67
+ 'id_text' => init[:id_text],
68
+ }
69
+ json = JSON.pretty_generate(info) + "\n"
70
+ fn_info = File.join(path, 'sgfa_binder.json')
71
+ File.open(fn_info, 'w', :encoding => 'utf-8'){|fi| fi.write json }
72
+
73
+ return id_hash
74
+ end # def create()
75
+
76
+
77
+ #####################################
78
+ # Open a binder
79
+ #
80
+ # @param path [String] Path to the binder
81
+ # @return [BinderFs] self
82
+ def open(path)
83
+ raise Error::Sanity, 'Binder already open' if @path
84
+
85
+ @lock = LockFs.new
86
+ begin
87
+ json = @lock.open(File.join(path, 'sgfa_binder.json'))
88
+ rescue Errno::ENOENT
89
+ raise Error::NonExistent, 'Binder does not exist'
90
+ end
91
+ begin
92
+ info = JSON.parse(json, :symbolize_names => true)
93
+ rescue JSON::NestingError, JSON::ParserError
94
+ @lock.close
95
+ @lock = nil
96
+ raise Error::Corrupt, 'Binder info corrupt'
97
+ end
98
+
99
+ @id_hash = info[:id_hash]
100
+ @id_text = info[:id_text]
101
+ @path = path.dup
102
+
103
+ return self
104
+ end # def open()
105
+
106
+
107
+ #####################################
108
+ # Close the binder
109
+ #
110
+ # @return [BinderFs] self
111
+ def close()
112
+ raise Error::Sanity, 'Binder not open' if !@path
113
+ @lock.close
114
+ @lock = nil
115
+ @id_hash = nil
116
+ @id_text = nil
117
+ @path = nil
118
+ return self
119
+ end # def close()
120
+
121
+
122
+ #####################################
123
+ # Temporary file to write attachments to
124
+ #
125
+ # @return [Tempfile] Temporary file
126
+ def temp()
127
+ raise Error::Sanity, 'Binder not open' if !@path
128
+ Tempfile.new('blob', @path, :encoding => 'utf-8')
129
+ end # def temp()
130
+
131
+
132
+ private
133
+
134
+
135
+ #####################################
136
+ # Create a jacket
137
+ def _jacket_create(num)
138
+ jp = File.join(@path, num.to_s)
139
+ id_text = 'binder %s jacket %d %s' %
140
+ [@id_hash, num, Time.now.utc.strftime('%F %T')]
141
+ id_hash = JacketFs.create(jp, id_text)
142
+ [id_text, id_hash]
143
+ end # def _jacket_create()
144
+
145
+
146
+ #####################################
147
+ # Open a jacket
148
+ def _jacket_open(num)
149
+ jck = JacketFs.new
150
+ jck.open(File.join(@path, num.to_s))
151
+ return jck
152
+ end # def _jacket_open()
153
+
154
+
155
+ #####################################
156
+ # Write cache
157
+ def _cache_write
158
+ jck = []
159
+ @jackets.each{|nam, hash| jck.push hash}
160
+ vals = []
161
+ @values.each{|val, sta| vals.push [val, sta]}
162
+ usrs = []
163
+ @users.each{|un, pl| usrs.push({name: un, perms: pl}) }
164
+ info = {
165
+ jackets: jck,
166
+ values: vals,
167
+ users: usrs,
168
+ }
169
+ json = JSON.pretty_generate(info) + "\n"
170
+
171
+ fnc = File.join(@path, 'cache.json')
172
+ File.open(fnc, 'w', :encoding => 'utf-8'){|fi| fi.write json}
173
+ end # def _cache_write
174
+
175
+
176
+ #####################################
177
+ # Read cache
178
+ def _cache_read
179
+ fnc = File.join(@path, 'cache.json')
180
+ json = nil
181
+ info = nil
182
+
183
+ begin
184
+ json = File.read(fnc, :encoding => 'utf-8')
185
+ info = JSON.parse(json, :symbolize_names => true)
186
+ rescue Error::ENOENT
187
+ raise Error::Corrupt, 'Binder cache does not exist'
188
+ rescue JSON::NestingError, JSON::ParserError
189
+ raise Error::Corrupt, 'Binder cache does not parse'
190
+ end
191
+
192
+ @jackets = {}
193
+ info[:jackets].each{|hash| @jackets[hash[:name]] = hash }
194
+ @users = {}
195
+ info[:users].each{|ha| @users[ha[:name]] = ha[:perms] }
196
+ @values = {}
197
+ info[:values].each{|val, sta| @values[val] = sta }
198
+ end # def _cache_read
199
+
200
+
201
+ end # class BinderFS
202
+
203
+ end # module Sgfa
@@ -0,0 +1,160 @@
1
+ #
2
+ # Simple Group of Filing Applications
3
+ # Command line interface for Binders
4
+ #
5
+ # Copyright (C) 2015 by Graham A. Field.
6
+ #
7
+ # See LICENSE.txt for licensing information.
8
+ #
9
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
10
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+
12
+ require 'thor'
13
+ require 'json'
14
+
15
+ require_relative '../binder_fs'
16
+ require_relative '../demo/web_binders'
17
+ require_relative '../demo/web_css'
18
+
19
+ module Sgfa
20
+ module Cli
21
+
22
+ #####################################################################
23
+ # Command line interface for {Sgfa::Binder}. Currently it just supports
24
+ # {Sgfa::BinderFs}.
25
+ #
26
+ # @todo Needs to be fully implemented. Currently just a shell.
27
+ class Binder < Thor
28
+
29
+ class_option :fs_path, {
30
+ type: :string,
31
+ desc: 'Path to the binder',
32
+ }
33
+
34
+ class_option :user, {
35
+ type: :string,
36
+ desc: 'User name.',
37
+ default: Etc.getpwuid(Process.euid).name,
38
+ }
39
+
40
+ class_option :body, {
41
+ type: :string,
42
+ desc: 'Body of an entry.',
43
+ default: '!! No body provided using CLI tool !!',
44
+ }
45
+
46
+ class_option :title, {
47
+ type: :string,
48
+ desc: 'Title of an entry.',
49
+ default: '!! No title provided using CLI tool !!',
50
+ }
51
+
52
+
53
+
54
+ #####################################
55
+ # info
56
+ desc 'info', 'Get basic information about the binder'
57
+ def info
58
+
59
+ # open binder
60
+ if !options[:fs_path]
61
+ puts 'Binder type and location required.'
62
+ return
63
+ end
64
+ bnd = ::Sgfa::BinderFs.new
65
+ begin
66
+ bnd.open(options[:fs_path])
67
+ rescue ::Sgfa::Error::Limits, ::Sgfa::Error::NonExistent => exp
68
+ puts exp.message
69
+ return
70
+ end
71
+
72
+ # print info
73
+ tr = {
74
+ perms: ['info']
75
+ }
76
+ info = bnd.binder_info(tr)
77
+ puts 'Text ID: %s' % info[:id_text]
78
+ puts 'Hash ID: %s' % info[:id_hash]
79
+ puts 'Values: %d' % info[:values].size
80
+ puts 'Jackets: %d' % info[:jackets].size
81
+ puts 'Users: %d' % info[:users].size
82
+
83
+ bnd.close
84
+ end # def info
85
+
86
+
87
+ #####################################
88
+ # create
89
+ desc 'create <id_text> <type_json>', 'Create a new Binder'
90
+ def create(id_text, init_json)
91
+
92
+ if !options[:fs_path]
93
+ puts 'Binder type and location required.'
94
+ return
95
+ end
96
+ bnd = ::Sgfa::BinderFs.new
97
+ tr = {
98
+ user: options[:user],
99
+ title: options[:title],
100
+ body: options[:body],
101
+ }
102
+ begin
103
+ init = JSON.parse(File.read(init_json), :symbolize_names => true)
104
+ init[:id_text] = id_text
105
+ bnd.create(options[:fs_path], tr, init)
106
+ rescue Errno::ENOENT
107
+ puts 'Binder type JSON file not found'
108
+ return
109
+ rescue JSON::JSONError
110
+ puts 'Binder type JSON file did not parse'
111
+ return
112
+ rescue ::Sgfa::Error::NonExistent, ::Sgfa::Error::Limits => exp
113
+ puts exp.message
114
+ return
115
+ end
116
+
117
+ end # def create()
118
+
119
+
120
+ #####################################
121
+ # web_demo
122
+ desc 'web_demo <css>', 'Run a demo web server'
123
+ method_option :addr, {
124
+ type: :string,
125
+ desc: 'Address to bind to',
126
+ default: 'localhost',
127
+ }
128
+ method_option :port, {
129
+ type: :numeric,
130
+ desc: 'Port to bind to',
131
+ default: '8888',
132
+ }
133
+ method_option :dir, {
134
+ type: :string,
135
+ desc: 'Path to the directory containing binders',
136
+ default: '.',
137
+ }
138
+ def web_demo(css)
139
+ begin
140
+ css_str = File.read(css)
141
+ rescue Errno::ENOENT
142
+ puts 'CSS file not found'
143
+ return
144
+ end
145
+
146
+ app_bnd = ::Sgfa::Demo::WebBinders.new(options[:dir], '/sgfa.css')
147
+ app_auth = Rack::Auth::Basic.new(app_bnd, 'Sgfa Demo'){|name, pass| true}
148
+ app_css = ::Sgfa::Demo::WebCss.new(css_str, '/sgfa.css', app_auth)
149
+
150
+ Rack::Handler::WEBrick.run(app_css, {
151
+ :Port => options[:port],
152
+ :BindAddress => options[:addr],
153
+ })
154
+
155
+ end # def web()
156
+
157
+ end # class Binder
158
+
159
+ end # module Cli
160
+ end # module Sgfa
@@ -0,0 +1,299 @@
1
+ #
2
+ # Simple Group of Filing Applications
3
+ # Command line interface for Jackets
4
+ #
5
+ # Copyright (C) 2015 by Graham A. Field.
6
+ #
7
+ # See LICENSE.txt for licensing information.
8
+ #
9
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
10
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+
12
+ require 'thor'
13
+
14
+ require_relative '../jacket_fs'
15
+
16
+ module Sgfa
17
+ module Cli
18
+
19
+ #####################################################################
20
+ # Command line interface for {Sgfa::Jacket}. Currently it just supports
21
+ # {Sgfa::JacketFs}.
22
+ #
23
+ # @todo Needs to be fully implemented. Currently just a shell.
24
+ class Jacket < Thor
25
+
26
+ class_option :fs_path, {
27
+ type: :string,
28
+ desc: 'Path to the jacket',
29
+ }
30
+
31
+ #####################################
32
+ # info
33
+ desc 'info', 'Get basic information about the jacket'
34
+ def info
35
+
36
+ # open jacket
37
+ jck = _open_jacket
38
+ return if !jck
39
+
40
+ # print info
41
+ hst = jck.read_history
42
+ puts 'Text ID: %s' % jck.id_text
43
+ puts 'Hash ID: %s' % jck.id_hash
44
+ if hst
45
+ puts 'Entries: %d' % hst.entry_max
46
+ puts 'History: %d' % hst.history
47
+ puts 'Last edit: %s' % hst.time_str
48
+ else
49
+ puts 'No history.'
50
+ end
51
+ jck.close
52
+
53
+ end # def info
54
+
55
+
56
+ #####################################
57
+ # Print Entry
58
+ desc 'entry <e-spec>', 'Print entry'
59
+ method_option :hash, {
60
+ type: :boolean,
61
+ desc: 'Display the entry hash or not',
62
+ default: false,
63
+ }
64
+ def entry(spec)
65
+
66
+ # get entry and revision nums
67
+ ma = /^\s*(\d+)(-(\d+))?\s*$/.match(spec)
68
+ if !ma
69
+ puts "Entry e-spec must be match format x[-y]\n" +
70
+ "Where x is the entry number and y is the optional revision number."
71
+ return
72
+ end
73
+ enum = ma[1].to_i
74
+ rnum = (ma.length == 4) ? ma[3].to_i : 0
75
+
76
+ # open jacket
77
+ jck = _open_jacket
78
+ return if !jck
79
+
80
+ # read
81
+ begin
82
+ ent = jck.read_entry(enum, rnum)
83
+ rescue ::Sgfa::Error::NonExistent => exp
84
+ puts exp.message
85
+ return
86
+ end
87
+ jck.close
88
+
89
+ # display
90
+ tags = ent.tags.join("\n ")
91
+ atts = ent.attachments.map{|anum, hnum, hash|
92
+ '%d-%d-%d %s' % [enum, anum, hnum, hash]}.join("\n ")
93
+ if options[:hash]
94
+ puts "Hash : %s" % ent.hash
95
+ end
96
+ puts "Title : %s" % ent.title
97
+ puts "Date/Time: %s" % ent.time.localtime.strftime("%F %T %z")
98
+ puts "Revision : %d" % ent.revision
99
+ puts "History : %d" % ent.history
100
+ puts "Tags : %s" % tags
101
+ puts "Files : %s" % atts
102
+ puts "Body :\n%s" % ent.body
103
+
104
+ end # def entry()
105
+
106
+
107
+ #####################################
108
+ # Display History
109
+ desc 'history [<hnum>]', 'Display the current or specified History'
110
+ method_option :hash, {
111
+ type: :boolean,
112
+ desc: 'Display the entry hash or not',
113
+ default: false,
114
+ }
115
+ def history(hspec='0')
116
+
117
+ # get history num
118
+ ma = /^\s*(\d+)\s*$/.match(hspec)
119
+ if !ma
120
+ hnum = 0
121
+ else
122
+ hnum = ma[1].to_i
123
+ end
124
+
125
+ # open jacket
126
+ jck = _open_jacket
127
+ return if !jck
128
+
129
+ # read history
130
+ begin
131
+ hst = jck.read_history(hnum)
132
+ rescue ::Sgfa::Error::NonExistent => exp
133
+ puts exp.message
134
+ return
135
+ end
136
+ jck.close
137
+
138
+ # display
139
+ hnum = hst.history
140
+ puts "Hash : %s" % hst.hash if options[:hash]
141
+ puts "Previous : %s" % hst.previous if options[:hash]
142
+ puts "History : %d" % hnum
143
+ puts "Date/Time : %s" % hst.time.localtime.strftime("%F %T %z")
144
+ puts "User : %s" % hst.user
145
+ hst.entries.each do |enum, rnum, hash|
146
+ puts "Entry : %d-%d %s" % [enum, rnum, hash]
147
+ end
148
+ hst.attachments.each do |enum, anum, hash|
149
+ puts "Attachment: %d-%d-%d %s" % [enum, anum, hnum, hash]
150
+ end
151
+
152
+ end # def history()
153
+
154
+
155
+ #####################################
156
+ # Output attachment
157
+ desc 'attach <a-spec>', 'Output attachment'
158
+ method_option :output, {
159
+ type: :string,
160
+ desc: 'Output file',
161
+ required: true
162
+ }
163
+ def attach(aspec)
164
+
165
+ # get attachment spec
166
+ ma = /^\s*(\d+)-(\d+)-(\d+)\s*$/.match(aspec)
167
+ if !ma
168
+ puts "Attachment specification is x-y-z\n" +
169
+ "x = entry number\ny = attachment number\nz = history number"
170
+ return
171
+ end
172
+ enum, anum, hnum = ma[1,3].map{|st| st.to_i }
173
+
174
+ # open jacket
175
+ jck = _open_jacket
176
+ return if !jck
177
+
178
+ # read attachment
179
+ begin
180
+ fi = jck.read_attach(enum, anum, hnum)
181
+ rescue ::Sgfa::Error::NonExistent => exp
182
+ puts exp.message
183
+ return
184
+ end
185
+ jck.close
186
+
187
+ # output
188
+ begin
189
+ out = File.open(options[:output], 'wb')
190
+ rescue
191
+ fi.close
192
+ puts "Unable to open output file"
193
+ return
194
+ end
195
+
196
+ # copy
197
+ IO.copy_stream(fi, out)
198
+ fi.close
199
+ out.close
200
+
201
+ end # def attach()
202
+
203
+
204
+ #####################################
205
+ # Check jacket
206
+ desc 'check', 'Checks jacket history chain'
207
+ method_option :max, {
208
+ type: :numeric,
209
+ desc: 'Maximum history to check',
210
+ }
211
+ method_option :min, {
212
+ type: :numeric,
213
+ desc: 'Minimum history to check',
214
+ }
215
+ method_option :hash, {
216
+ type: :string,
217
+ desc: 'Known good hash of maximum history',
218
+ }
219
+ method_option :entry, {
220
+ type: :boolean,
221
+ desc: 'Hash entries and validate',
222
+ default: true,
223
+ }
224
+ method_option :attach, {
225
+ type: :boolean,
226
+ desc: 'Hash attachments and validate',
227
+ default: false,
228
+ }
229
+ method_option :missing, {
230
+ type: :numeric,
231
+ desc: 'Number of missing history items to check',
232
+ default: 0,
233
+ }
234
+ method_option :level, {
235
+ type: :string,
236
+ desc: 'Debug level, "debug", "info", "warn", "error"',
237
+ default: 'error',
238
+ }
239
+ def check
240
+
241
+ # open jacket
242
+ jck = _open_jacket
243
+ return if !jck
244
+
245
+ # options
246
+ opts = {}
247
+ log = Logger.new(STDOUT)
248
+ opts[:log] = log
249
+ case options[:level]
250
+ when 'debug'
251
+ log.level = Logger::DEBUG
252
+ when 'info'
253
+ log.level = Logger::INFO
254
+ when 'warn'
255
+ log.level = Logger::WARN
256
+ else
257
+ log.level = Logger::ERROR
258
+ end
259
+ opts[:hash_entry] = options[:entry]
260
+ opts[:hash_attach] = options[:attach]
261
+ opts[:miss_history] = options[:missing]
262
+ opts[:max_history] = options[:max] if options[:max]
263
+ opts[:min_history] = options[:min] if options[:min]
264
+ opts[:max_hash] = options[:hash] if options[:hash]
265
+
266
+ # check
267
+ jck.check(opts)
268
+ jck.close
269
+
270
+ end # def check()
271
+
272
+
273
+
274
+ private
275
+
276
+
277
+ #####################################
278
+ # Open the jacket
279
+ def _open_jacket()
280
+ if !options[:fs_path]
281
+ puts 'Jacket type and location required.'
282
+ return false
283
+ end
284
+
285
+ begin
286
+ jck = ::Sgfa::JacketFs.new(options[:fs_path])
287
+ rescue ::Sgfa::Error::NonExistent, ::Sgfa::Error::Limits => exp
288
+ puts exp.message
289
+ return false
290
+ end
291
+
292
+ return jck
293
+ end # def _open_jacket
294
+
295
+
296
+ end # class Jacket
297
+
298
+ end # module Cli
299
+ end # module Sgfa
data/lib/sgfa/cli.rb ADDED
@@ -0,0 +1,36 @@
1
+ #
2
+ # Simple Group of Filing Applications
3
+ # Command line interface tool
4
+ #
5
+ # Copyright (C) 2015 by Graham A. Field.
6
+ #
7
+ # See LICENSE.txt for licensing information.
8
+ #
9
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
10
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+
12
+ require 'thor'
13
+
14
+ require_relative 'cli/jacket'
15
+ require_relative 'cli/binder'
16
+
17
+ module Sgfa
18
+
19
+ #####################################################################
20
+ # Command line interface for Sgfa tools
21
+ module Cli
22
+
23
+ #####################################################################
24
+ # Top level CLI
25
+ class Sgfa < Thor
26
+
27
+ desc 'jacket ...', 'Jacket CLI tools'
28
+ subcommand 'jacket', Jacket
29
+
30
+ desc 'binder ...', 'Binder CLI tools'
31
+ subcommand 'binder', Binder
32
+
33
+ end # class Sgfa
34
+
35
+ end # module Cli
36
+ end # module Sgfa