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