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
data/lib/sgfa/lock_fs.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
#
|
2
|
+
# Simple Group of Filing Applications
|
3
|
+
# Locking using filesystem locks
|
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_relative 'error'
|
13
|
+
|
14
|
+
module Sgfa
|
15
|
+
|
16
|
+
|
17
|
+
##########################################################################
|
18
|
+
# Lock based on file locks
|
19
|
+
class LockFs
|
20
|
+
|
21
|
+
|
22
|
+
################################
|
23
|
+
# Lock state
|
24
|
+
#
|
25
|
+
# @return [Symbol] :closed, :unlocked, :shared, or :exclusive
|
26
|
+
attr_reader :state
|
27
|
+
|
28
|
+
|
29
|
+
################################
|
30
|
+
# Initialize
|
31
|
+
def initialize
|
32
|
+
@file = nil
|
33
|
+
@state = :closed
|
34
|
+
end # def initialize
|
35
|
+
|
36
|
+
|
37
|
+
################################
|
38
|
+
# Open lock file
|
39
|
+
#
|
40
|
+
# @todo Handle exceptions from File.open()
|
41
|
+
#
|
42
|
+
# @param fnam [String] File name of lock file
|
43
|
+
# @return [String] Contents of the lock file
|
44
|
+
# @raise [Error::Sanity] if lock already open
|
45
|
+
def open(fnam)
|
46
|
+
raise Error::Sanity, 'Lock file already open' if @file
|
47
|
+
@file = File.open(fnam, 'r', :encoding => 'utf-8')
|
48
|
+
@state = :unlocked
|
49
|
+
return @file.read
|
50
|
+
end # def open()
|
51
|
+
|
52
|
+
|
53
|
+
################################
|
54
|
+
# Close lock file
|
55
|
+
#
|
56
|
+
# @raise [Error::Sanity] if lock not open
|
57
|
+
def close
|
58
|
+
raise Error::Sanity, 'Lock file not open' if !@file
|
59
|
+
@file.close
|
60
|
+
@file = nil
|
61
|
+
@state = :closed
|
62
|
+
end # def close
|
63
|
+
|
64
|
+
|
65
|
+
################################
|
66
|
+
# Take exclusive lock
|
67
|
+
#
|
68
|
+
# @raise [Error::Sanity] if lock not open
|
69
|
+
def exclusive
|
70
|
+
raise Error::Sanity, 'Lock file not open' if !@file
|
71
|
+
|
72
|
+
if @state == :exclusive
|
73
|
+
return
|
74
|
+
elsif @state == :shared
|
75
|
+
@file.flock(File::LOCK_UN)
|
76
|
+
end
|
77
|
+
|
78
|
+
@file.flock(File::LOCK_EX)
|
79
|
+
@state = :exclusive
|
80
|
+
end # def exclusive
|
81
|
+
|
82
|
+
|
83
|
+
################################
|
84
|
+
# Take shared lock
|
85
|
+
#
|
86
|
+
# @raise [Error::Sanity] if lock not open
|
87
|
+
def shared
|
88
|
+
raise Error::Sanity, 'Lock file not open' if !@file
|
89
|
+
return if @state == :shared
|
90
|
+
@file.flock(File::LOCK_SH)
|
91
|
+
@state = :shared
|
92
|
+
end # def shared
|
93
|
+
|
94
|
+
|
95
|
+
################################
|
96
|
+
# Release lock
|
97
|
+
#
|
98
|
+
# @raise [Error::Sanity] if lock not open
|
99
|
+
def unlock
|
100
|
+
raise Error::Sanity, 'Lock file not open' if !@file
|
101
|
+
return if @state == :unlocked
|
102
|
+
@file.flock(File::LOCK_UN)
|
103
|
+
@state = :unlocked
|
104
|
+
end # def unlock
|
105
|
+
|
106
|
+
|
107
|
+
################################
|
108
|
+
# Run block while holding exclusive lock
|
109
|
+
#
|
110
|
+
# @param rel [Boolean] Release lock on return
|
111
|
+
# @raise (see #exclusive)
|
112
|
+
def do_ex(rel=true)
|
113
|
+
exclusive
|
114
|
+
begin
|
115
|
+
ret = yield
|
116
|
+
ensure
|
117
|
+
unlock if rel
|
118
|
+
end
|
119
|
+
return ret
|
120
|
+
end # def do_ex
|
121
|
+
|
122
|
+
|
123
|
+
################################
|
124
|
+
# Run block while holding shared lock
|
125
|
+
#
|
126
|
+
# @param rel [Boolean] Release lock on return
|
127
|
+
# @riase (see #shared)
|
128
|
+
def do_sh(rel=true)
|
129
|
+
shared
|
130
|
+
begin
|
131
|
+
ret = yield
|
132
|
+
ensure
|
133
|
+
unlock if rel
|
134
|
+
end
|
135
|
+
return ret
|
136
|
+
end # def do_sh
|
137
|
+
|
138
|
+
|
139
|
+
end # class LockFs
|
140
|
+
|
141
|
+
end # module Sgfa
|
@@ -0,0 +1,342 @@
|
|
1
|
+
#
|
2
|
+
# Simple Group of Filing Applications
|
3
|
+
# Jacket state using filesystem storage.
|
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 'fileutils'
|
13
|
+
require 'tempfile'
|
14
|
+
|
15
|
+
require_relative 'error'
|
16
|
+
|
17
|
+
module Sgfa
|
18
|
+
|
19
|
+
|
20
|
+
#####################################################################
|
21
|
+
# Maintains {Jacket} state using filesystem storage.
|
22
|
+
#
|
23
|
+
# This stores the current {History} number for a {Jacket} as well as
|
24
|
+
# the current revision numbers for each {Entry}. It also maintains
|
25
|
+
# for each tag, a date/time ordered list of all {Entry} items where
|
26
|
+
# the current revision contains that tag. Finally, it keeps a list of
|
27
|
+
# all tags which are present in the {Jacket}.
|
28
|
+
#
|
29
|
+
# The current revision of each entry is stored in a file named
|
30
|
+
# "_state" with each line consisting of a 9-digit current
|
31
|
+
# revision (zero padded) followed by newline. The current {History}
|
32
|
+
# number is stored on line zero, with each {Entry} on the corresponding
|
33
|
+
# line.
|
34
|
+
#
|
35
|
+
# The list of tags is in a file named "_list" and consists of
|
36
|
+
# each tag followed by a newline.
|
37
|
+
#
|
38
|
+
# Each tag is kept in a seperate file, named with the tag name. Each
|
39
|
+
# line consists of a UTC date/time in "YYYY-MM-DD HH:MM:SS" format,
|
40
|
+
# followed by a space, and the 9-digit (zero padded) {Entry} number,
|
41
|
+
# with a newline. A "_all" tag tracks all {Entry}s.
|
42
|
+
#
|
43
|
+
class StateFs
|
44
|
+
|
45
|
+
|
46
|
+
# Tag which has all {Entry} items in a {Jacket}
|
47
|
+
TagAll = '_all'
|
48
|
+
|
49
|
+
# File name to store list of all tags
|
50
|
+
TagList = '_list'
|
51
|
+
|
52
|
+
# File name used to store the current {History} and {Entry} revision
|
53
|
+
# numbers
|
54
|
+
TagState = '_state'
|
55
|
+
|
56
|
+
# Size (bytes) of each entry revision
|
57
|
+
EntrySize = 10
|
58
|
+
|
59
|
+
# Size (bytes) of each tag date/time and entry listing
|
60
|
+
TagSize = 30
|
61
|
+
|
62
|
+
# Regular expression to get date/time string and entry number
|
63
|
+
# from a tag file
|
64
|
+
TagReg = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (\d{9})$/
|
65
|
+
|
66
|
+
private_constant :TagAll, :TagList, :TagState, :EntrySize, :TagSize,
|
67
|
+
:TagReg
|
68
|
+
|
69
|
+
|
70
|
+
#####################################
|
71
|
+
# Create a new state
|
72
|
+
#
|
73
|
+
# @param path [String] Path to the directory containing the state
|
74
|
+
# @raise [Error::Conflict] if path already exists
|
75
|
+
def self.create(path)
|
76
|
+
begin
|
77
|
+
Dir.mkdir(path)
|
78
|
+
rescue Errno::EEXIST
|
79
|
+
raise Error::Conflict, 'State path already exists'
|
80
|
+
end
|
81
|
+
FileUtils.touch(File.join(path, TagList))
|
82
|
+
File.open(File.join(path, TagState), 'wb'){|fi| fi.write "000000000\n"}
|
83
|
+
end # def self.create()
|
84
|
+
|
85
|
+
|
86
|
+
#####################################
|
87
|
+
# Initialize new state, optionally opening
|
88
|
+
#
|
89
|
+
# @param path [String] Path to the directory containing the state
|
90
|
+
# @raise (see #open)
|
91
|
+
def initialize(path=nil)
|
92
|
+
open(path) if path
|
93
|
+
end # def initialize()
|
94
|
+
|
95
|
+
|
96
|
+
#####################################
|
97
|
+
# Open the state
|
98
|
+
#
|
99
|
+
# @param path [String] Path to the directory containing the state
|
100
|
+
# @raise [Error::NonExistent] if path does not exist
|
101
|
+
# @return [StateFs] self
|
102
|
+
def open(path)
|
103
|
+
fn = File.join(path, TagState)
|
104
|
+
begin
|
105
|
+
@file = File.open(fn, 'r+b')
|
106
|
+
rescue Errno::ENOENT
|
107
|
+
raise Error::NonExistent, 'State path does not exist'
|
108
|
+
end
|
109
|
+
@path = path.dup
|
110
|
+
return self
|
111
|
+
end # def open()
|
112
|
+
|
113
|
+
|
114
|
+
#####################################
|
115
|
+
# Close the state
|
116
|
+
#
|
117
|
+
# @raise [Error::Sanity] if state is not open
|
118
|
+
# @return [StateFs] self
|
119
|
+
def close
|
120
|
+
raise Error::Sanity, 'State not open' if !@path
|
121
|
+
@file.close
|
122
|
+
@path = nil
|
123
|
+
return self
|
124
|
+
end # def close
|
125
|
+
|
126
|
+
|
127
|
+
#####################################
|
128
|
+
# Reset to empty state
|
129
|
+
#
|
130
|
+
# @raise [Error::Sanity] if state is not open
|
131
|
+
# @return (see #open)
|
132
|
+
def reset
|
133
|
+
raise Error::Sanity, 'State not open' if !@path
|
134
|
+
|
135
|
+
path = @path
|
136
|
+
self.close
|
137
|
+
|
138
|
+
FileUtils.remove_dir(path)
|
139
|
+
Dir.mkdir(path)
|
140
|
+
FileUtils.touch(File.join(path, TagList))
|
141
|
+
File.open(File.join(path, TagState), 'wb'){|fi| fi.write "000000000\n"}
|
142
|
+
|
143
|
+
return open(path)
|
144
|
+
end # def reset
|
145
|
+
|
146
|
+
|
147
|
+
#####################################
|
148
|
+
# Set current revision number for an entry
|
149
|
+
#
|
150
|
+
# @param enum [Integer] Entry number
|
151
|
+
# @param rnum [Integer] Current revision number
|
152
|
+
# @raise [Error::Sanity] if state is not open
|
153
|
+
# @raise [ArgumentError] if enum or rnum is negative
|
154
|
+
# @return [StateFs] self
|
155
|
+
def set(enum, rnum)
|
156
|
+
raise Error::Sanity, 'State not open' if !@path
|
157
|
+
if enum < 0 || rnum < 0
|
158
|
+
raise ArgumentError, 'Invalid entry/revision numbers'
|
159
|
+
end
|
160
|
+
|
161
|
+
@file.seek(enum*EntrySize, IO::SEEK_SET)
|
162
|
+
@file.write("%09d\n" % rnum)
|
163
|
+
return self
|
164
|
+
end # def set
|
165
|
+
|
166
|
+
|
167
|
+
#####################################
|
168
|
+
# Get current revision number for an entry
|
169
|
+
#
|
170
|
+
# @param enum [Integer] Entry number
|
171
|
+
# @return [Integer] Current revision number
|
172
|
+
# @raise [Error::Sanity] if state not open
|
173
|
+
# @raise [ArgumentError] if enum is negative
|
174
|
+
# @raise [Error::NonExistent] if entry number does not exist
|
175
|
+
def get(enum)
|
176
|
+
raise Error::Sanity, 'State not open' if !@path
|
177
|
+
raise ArgumentError, 'Invalid entry number' if enum < 0
|
178
|
+
|
179
|
+
@file.seek(enum*EntrySize, IO::SEEK_SET)
|
180
|
+
res = @file.read(EntrySize)
|
181
|
+
raise Error::NonExistent, 'Entry does not exist' if !res || res[0] == "\x00"
|
182
|
+
return res.to_i
|
183
|
+
end # def get()
|
184
|
+
|
185
|
+
|
186
|
+
#####################################
|
187
|
+
# Read list of tags
|
188
|
+
#
|
189
|
+
# @return [Array] List of strings containing tag names
|
190
|
+
# @raise [Error::Sanity] if state not open
|
191
|
+
# @raise [Error::Corrupt] if tag list is missing
|
192
|
+
def list
|
193
|
+
raise Error::Sanity, 'State not open' if !@path
|
194
|
+
|
195
|
+
ftn = File.join(@path, TagList)
|
196
|
+
begin
|
197
|
+
txt = File.read(ftn)
|
198
|
+
rescue Errno::ENOENT
|
199
|
+
raise Error::Corrupt, 'Unable to read tag list'
|
200
|
+
end
|
201
|
+
return txt.lines.map{|tg| tg.chomp }
|
202
|
+
end # def list
|
203
|
+
|
204
|
+
|
205
|
+
#####################################
|
206
|
+
# Read entry numbers from a tag
|
207
|
+
#
|
208
|
+
# @param name [String] Tag name
|
209
|
+
# @param offs [Integer] Offset to begin reading
|
210
|
+
# @param max [Integer] Maximum number of entries to return
|
211
|
+
# @raise [Error::Sanity] if state not open
|
212
|
+
# @raise [Error::NonExistent] if tag does not exist
|
213
|
+
# @raise [Error::Corrupt] if file format is bad
|
214
|
+
# @return [Array] Total number of entries in the tag, and possibly empty
|
215
|
+
# array of entry numbers.
|
216
|
+
def tag(name, offs, max)
|
217
|
+
raise Error::Sanity, 'State not open' if !@path
|
218
|
+
|
219
|
+
fn = File.join(@path, name)
|
220
|
+
begin
|
221
|
+
fi = File.open(fn, 'rb')
|
222
|
+
rescue Errno::ENOENT
|
223
|
+
raise Error::NonExistent, 'Tag does not exist'
|
224
|
+
end
|
225
|
+
|
226
|
+
ents = []
|
227
|
+
size = nil
|
228
|
+
begin
|
229
|
+
size = fi.size / TagSize
|
230
|
+
return [size, ents] if( offs > size || max == 0 )
|
231
|
+
num = (offs+max > size) ? (size - offs) : max
|
232
|
+
fi.seek(((size-offs-num)*TagSize), IO::SEEK_SET)
|
233
|
+
num.times do
|
234
|
+
ln = fi.read(TagSize)
|
235
|
+
ma = TagReg.match(ln)
|
236
|
+
raise Error::Corrupt, 'Bad tag format' unless ma
|
237
|
+
ents.push( ma[2].to_i )
|
238
|
+
end
|
239
|
+
ensure
|
240
|
+
fi.close
|
241
|
+
end
|
242
|
+
|
243
|
+
return [size, ents]
|
244
|
+
end # def tag()
|
245
|
+
|
246
|
+
|
247
|
+
#####################################
|
248
|
+
# Update tags based on new {History}
|
249
|
+
#
|
250
|
+
# @param cng [Hash] Changes in the format returned by {History#next}
|
251
|
+
# @raise [Error::Sanity] if state not open
|
252
|
+
# @raise [Error::Corrupt] if file format is bad
|
253
|
+
# @raise [Error::Corrupt] if list of tags is missing
|
254
|
+
# @raise [Error::Corrupt] if existing tag is missing
|
255
|
+
# @return [StateFs] self
|
256
|
+
def update(cng)
|
257
|
+
|
258
|
+
# read list of tags
|
259
|
+
changed = false
|
260
|
+
thash = {}
|
261
|
+
self.list.each{|tag| thash[tag] = true }
|
262
|
+
|
263
|
+
cng.each do |tag, hc|
|
264
|
+
cnt = 0
|
265
|
+
|
266
|
+
# sorted changes
|
267
|
+
se = hc.to_a.select{|en, ti| ti }.sort{|aa, bb| aa[1] <=> bb[1] }
|
268
|
+
|
269
|
+
# files
|
270
|
+
fn = File.join(@path, tag)
|
271
|
+
if thash[tag]
|
272
|
+
begin
|
273
|
+
oldf = File.open(fn, 'rb')
|
274
|
+
rescue Errno::ENOENT
|
275
|
+
raise Error::Corrupt, 'Existing tag is missing'
|
276
|
+
end
|
277
|
+
else
|
278
|
+
oldf = nil
|
279
|
+
end
|
280
|
+
newf = Tempfile.new('state', @path, :encoding => 'ASCII-8BIT')
|
281
|
+
|
282
|
+
# merge new into old file
|
283
|
+
while oldf && ln = oldf.read(TagSize)
|
284
|
+
unless ma = TagReg.match(ln)
|
285
|
+
newf.close!
|
286
|
+
raise Error::Corrupt, 'Bad tag format'
|
287
|
+
end
|
288
|
+
|
289
|
+
tme = ma[1]
|
290
|
+
enum = ma[2].to_i
|
291
|
+
|
292
|
+
next if hc.has_key?(enum)
|
293
|
+
|
294
|
+
while se.size > 0 && tme > se[0][1]
|
295
|
+
ne, nt = se.shift
|
296
|
+
newf.write("%s %09d\n" % [nt, ne])
|
297
|
+
cnt += 1
|
298
|
+
end
|
299
|
+
|
300
|
+
newf.write(ln)
|
301
|
+
cnt += 1
|
302
|
+
end
|
303
|
+
oldf.close() if oldf
|
304
|
+
|
305
|
+
# write out any remaining new
|
306
|
+
while ary = se.shift
|
307
|
+
ne, nt = ary
|
308
|
+
newf.write("%s %09d\n" % [nt, ne])
|
309
|
+
cnt += 1
|
310
|
+
end
|
311
|
+
|
312
|
+
# adjust files
|
313
|
+
if cnt == 0
|
314
|
+
if oldf
|
315
|
+
File.unlink(fn)
|
316
|
+
thash.delete(tag)
|
317
|
+
changed = true
|
318
|
+
end
|
319
|
+
else
|
320
|
+
FileUtils.ln(newf.path, fn, :force => true)
|
321
|
+
if !oldf
|
322
|
+
thash[tag] = true
|
323
|
+
changed = true
|
324
|
+
end
|
325
|
+
end
|
326
|
+
newf.close!
|
327
|
+
end
|
328
|
+
|
329
|
+
# write list of tags
|
330
|
+
if changed
|
331
|
+
fnl = File.join(@path, TagList)
|
332
|
+
File.open(fnl, 'w', :encoding => 'utf-8') do |fi|
|
333
|
+
thash.each_key{|tag| fi.puts tag }
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
return self
|
338
|
+
end # def update()
|
339
|
+
|
340
|
+
end # class StateFs
|
341
|
+
|
342
|
+
end # module Sgfa
|
@@ -0,0 +1,214 @@
|
|
1
|
+
#
|
2
|
+
# Simple Group of Filing Applications
|
3
|
+
# Jacket store using filesystem storage.
|
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 'tempfile'
|
13
|
+
require 'fileutils'
|
14
|
+
|
15
|
+
require_relative 'error'
|
16
|
+
|
17
|
+
module Sgfa
|
18
|
+
|
19
|
+
|
20
|
+
#####################################################################
|
21
|
+
# Stores copies of {History}, {Entry}, and attached files that are
|
22
|
+
# in a {Jacket} using filesystem storage.
|
23
|
+
#
|
24
|
+
# Storage and retrieval are based on type (i.e. History, Entry, or
|
25
|
+
# attached file) and the item hash. The item hash is generated from
|
26
|
+
# the {Jacket} id_hash, the type, and the specific number. This allows
|
27
|
+
# for a single store to serve as the repository of multiple {Jacket}s
|
28
|
+
# if desired (e.g. using a cloud permanent object store).
|
29
|
+
#
|
30
|
+
# Each item is stored in a file, organized into directories based on the
|
31
|
+
# first two characters of the item hash, a file name consisting of the
|
32
|
+
# remaining item hash, dash, and a character indicating the type.
|
33
|
+
#
|
34
|
+
class StoreFs
|
35
|
+
|
36
|
+
|
37
|
+
#####################################
|
38
|
+
# Initialize a new store object, optionally opening
|
39
|
+
#
|
40
|
+
# @param (see #open)
|
41
|
+
# @raise (see #open)
|
42
|
+
def initialize(path=nil)
|
43
|
+
open(path) if path
|
44
|
+
end # def initialize()
|
45
|
+
|
46
|
+
|
47
|
+
#####################################
|
48
|
+
# Create a new store
|
49
|
+
# @param path [String] Path to the directory containing the store
|
50
|
+
# @raise [Error::Conflict] if path already exists
|
51
|
+
def self.create(path)
|
52
|
+
begin
|
53
|
+
Dir.mkdir(path)
|
54
|
+
rescue Errno::EEXIST
|
55
|
+
raise Error::Conflict, 'Store already exists'
|
56
|
+
end
|
57
|
+
end # def self.create()
|
58
|
+
|
59
|
+
|
60
|
+
#####################################
|
61
|
+
# Open the store
|
62
|
+
#
|
63
|
+
# @param path [String] Path to the directory containing the store
|
64
|
+
# @raise [Error::NonExistent] if path does not exist
|
65
|
+
# @return [StoreFs] self
|
66
|
+
def open(path)
|
67
|
+
raise Error::NonExistent, 'Store does not exist' if !File.directory?(path)
|
68
|
+
@path = path
|
69
|
+
return self
|
70
|
+
end # open()
|
71
|
+
|
72
|
+
|
73
|
+
#####################################
|
74
|
+
# Close the store
|
75
|
+
#
|
76
|
+
# @raise [Error::Sanity] if store is not open
|
77
|
+
# @return [StoreFs] self
|
78
|
+
def close
|
79
|
+
raise Error::Sanity, 'Store is not open' if !@path
|
80
|
+
@path = nil
|
81
|
+
return self
|
82
|
+
end # def close
|
83
|
+
|
84
|
+
|
85
|
+
#####################################
|
86
|
+
# File name for an item
|
87
|
+
#
|
88
|
+
# @param type [Symbol] Type of the item
|
89
|
+
# @param item [String] Item hash identifier
|
90
|
+
# @raise [NotImplementedError] if type is not valid
|
91
|
+
# @return [String] the file name
|
92
|
+
def _fn(type, item)
|
93
|
+
case type
|
94
|
+
when :entry then ext = 'e'
|
95
|
+
when :history then ext = 'h'
|
96
|
+
when :file then ext = 'f'
|
97
|
+
else raise NotImplementedError, 'Invalid item type'
|
98
|
+
end
|
99
|
+
|
100
|
+
return '%s/%s/%s-%s' % [@path, item[0,2], item[2..-1], ext]
|
101
|
+
end # def _fn()
|
102
|
+
private :_fn
|
103
|
+
|
104
|
+
|
105
|
+
#####################################
|
106
|
+
# Get a temp file to use to create an item for storage
|
107
|
+
#
|
108
|
+
# @note Encoding on the file is set to 'utf-8' by default
|
109
|
+
# @raise [Error::Sanity] if store not open
|
110
|
+
# @return [Tempfile] the temporary file
|
111
|
+
def temp
|
112
|
+
raise Error::Sanity, 'Store not open' if !@path
|
113
|
+
Tempfile.new('blob', @path, :encoding => 'utf-8')
|
114
|
+
end # def temp
|
115
|
+
|
116
|
+
|
117
|
+
#####################################
|
118
|
+
# Read an item
|
119
|
+
#
|
120
|
+
# @param (see #_fn)
|
121
|
+
# @raise [NotImplementedError] if type is not valid
|
122
|
+
# @raise [Error::Sanity] if store not open
|
123
|
+
# @return [File] Item opened in read only format or false if not found
|
124
|
+
def read(type, item)
|
125
|
+
raise Error::Sanity, 'Store not open' if !@path
|
126
|
+
|
127
|
+
fn = _fn(type, item)
|
128
|
+
begin
|
129
|
+
fi = File.open(fn, 'r', :encoding => 'utf-8')
|
130
|
+
rescue Errno::ENOENT
|
131
|
+
return false
|
132
|
+
end
|
133
|
+
return fi
|
134
|
+
end # def read()
|
135
|
+
|
136
|
+
|
137
|
+
#####################################
|
138
|
+
# Write an item
|
139
|
+
#
|
140
|
+
# If content is a string, it will be written to a file and saved.
|
141
|
+
# If content is a file it will be deleted from it's current location.
|
142
|
+
#
|
143
|
+
# @param (see #_fn)
|
144
|
+
# @param cont [File, String] Content to store
|
145
|
+
# @raise [Error::Sanity] if store not open
|
146
|
+
# @raise [NotImplementedError] if type is not valid
|
147
|
+
def write(type, item, cont)
|
148
|
+
raise Error::Sanity, 'Store not open' if !@path
|
149
|
+
fn = _fn(type, item)
|
150
|
+
|
151
|
+
if cont.is_a?(String)
|
152
|
+
tf = temp
|
153
|
+
tf.write cont
|
154
|
+
cont = tf
|
155
|
+
end
|
156
|
+
|
157
|
+
begin
|
158
|
+
FileUtils.ln(cont.path, fn, :force => true)
|
159
|
+
rescue Errno::ENOENT
|
160
|
+
Dir.mkdir(File.dirname(fn))
|
161
|
+
FileUtils.ln(cont.path, fn)
|
162
|
+
end
|
163
|
+
if cont.respond_to?( :close! )
|
164
|
+
cont.close!
|
165
|
+
else
|
166
|
+
File.unlink(cont, path)
|
167
|
+
cont.close
|
168
|
+
end
|
169
|
+
|
170
|
+
return self
|
171
|
+
end # def write()
|
172
|
+
|
173
|
+
|
174
|
+
#####################################
|
175
|
+
# Delete an item
|
176
|
+
#
|
177
|
+
# @param (see #_fn)
|
178
|
+
# @raise [NotImplementedError] if type is not valid
|
179
|
+
# @raise [Error::Sanity] if store not open
|
180
|
+
# @return [Boolean] if item deleted
|
181
|
+
def delete(type, item)
|
182
|
+
raise Error::Sanity, 'Store not open' if !@path
|
183
|
+
fn = _fn(type, item)
|
184
|
+
|
185
|
+
begin
|
186
|
+
File.unlink(fn)
|
187
|
+
rescue Errno::ENOENT
|
188
|
+
return false
|
189
|
+
end
|
190
|
+
return true
|
191
|
+
end # def delete()
|
192
|
+
|
193
|
+
|
194
|
+
#####################################
|
195
|
+
# Get size of an item in bytes
|
196
|
+
#
|
197
|
+
# @param (see #_fn)
|
198
|
+
# @raise [NotImplementedError] if type is not valid
|
199
|
+
# @raise [Error::Sanity] if store not open
|
200
|
+
# @return [Integer, Boolean] Item size, or false if item does not exist
|
201
|
+
def size(type, item)
|
202
|
+
fn = _fn(type, item)
|
203
|
+
begin
|
204
|
+
size = File.size(fn)
|
205
|
+
rescue Errno::ENOENT
|
206
|
+
return false
|
207
|
+
end
|
208
|
+
return size
|
209
|
+
end # def size()
|
210
|
+
|
211
|
+
|
212
|
+
end # class StoreFs
|
213
|
+
|
214
|
+
end # module Sgfa
|