sgfa 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.
@@ -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