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
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
|