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.
- checksums.yaml +7 -0
- data/LICENSE.txt +674 -0
- data/README.txt +14 -0
- data/bin/sgfa +15 -0
- data/data/sgfa_web.css +240 -0
- data/lib/sgfa/binder.rb +627 -0
- data/lib/sgfa/binder_fs.rb +203 -0
- data/lib/sgfa/cli/binder.rb +160 -0
- data/lib/sgfa/cli/jacket.rb +299 -0
- data/lib/sgfa/cli.rb +36 -0
- data/lib/sgfa/demo/web_binders.rb +111 -0
- data/lib/sgfa/demo/web_css.rb +60 -0
- data/lib/sgfa/entry.rb +697 -0
- data/lib/sgfa/error.rb +95 -0
- data/lib/sgfa/history.rb +445 -0
- data/lib/sgfa/jacket.rb +556 -0
- data/lib/sgfa/jacket_fs.rb +136 -0
- data/lib/sgfa/lock_fs.rb +141 -0
- data/lib/sgfa/state_fs.rb +342 -0
- data/lib/sgfa/store_fs.rb +214 -0
- data/lib/sgfa/web/base.rb +225 -0
- data/lib/sgfa/web/binder.rb +1190 -0
- data/lib/sgfa.rb +37 -0
- metadata +68 -0
@@ -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
|