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/entry.rb
ADDED
@@ -0,0 +1,697 @@
|
|
1
|
+
#
|
2
|
+
# Simple Group of Filing Applications
|
3
|
+
# Entry item
|
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 'digest/sha2'
|
13
|
+
require 'json'
|
14
|
+
|
15
|
+
require_relative 'error'
|
16
|
+
|
17
|
+
module Sgfa
|
18
|
+
|
19
|
+
|
20
|
+
#####################################################################
|
21
|
+
# The Entry is the basic item which is being filed in a {Jacket}.
|
22
|
+
#
|
23
|
+
# The Entry has attributes:
|
24
|
+
# * hash - SHA256 hash of the canonical encoding
|
25
|
+
# * canonical - the canonical encoded string
|
26
|
+
# * jacket - the {Jacket} hash ID the entry belongs to
|
27
|
+
# * entry - the entry number
|
28
|
+
# * revision - the revision number
|
29
|
+
# * history - the history number where the entry was recorded
|
30
|
+
# * title - one line description
|
31
|
+
# * time - date/time used to sort within a tag
|
32
|
+
# * time_str - encoded version of the time in UTC
|
33
|
+
# * body - multiple line text of the entry
|
34
|
+
# * tags - list of all associated tags (may be empty)
|
35
|
+
# * attachments - list of attached files with names (may be empty)
|
36
|
+
# * max_attach - the maximum number of attachments ever belonging to
|
37
|
+
# the entry
|
38
|
+
#
|
39
|
+
class Entry
|
40
|
+
|
41
|
+
|
42
|
+
#########################################################
|
43
|
+
# @!group Limits checks
|
44
|
+
|
45
|
+
# Max chars in title
|
46
|
+
LimTitleMax = 128
|
47
|
+
|
48
|
+
# Invalid chars in title
|
49
|
+
LimTitleInv = /[[:cntrl:]]/
|
50
|
+
|
51
|
+
#####################################
|
52
|
+
# Limit check, title
|
53
|
+
def self.limits_title(str)
|
54
|
+
Error.limits(str, 1, LimTitleMax, LimTitleInv, 'Entry title')
|
55
|
+
end # def self.limits_title()
|
56
|
+
|
57
|
+
|
58
|
+
# Max chars in body
|
59
|
+
LimBodyMax = 1024 * 8
|
60
|
+
|
61
|
+
# Invalid chars in body
|
62
|
+
LimBodyInv = /[^[:print:][:space:]]/
|
63
|
+
|
64
|
+
#####################################
|
65
|
+
# Limit check, body
|
66
|
+
def self.limits_body(str)
|
67
|
+
Error.limits(str, 1, LimBodyMax, LimBodyInv, 'Entry body')
|
68
|
+
end # def self.limits_body()
|
69
|
+
|
70
|
+
|
71
|
+
# Max chars in a tag
|
72
|
+
LimTagMax = 128
|
73
|
+
|
74
|
+
# Invalid chars in a tag
|
75
|
+
LimTagInv = /[[:cntrl:]\/\\\*\?]|^_/
|
76
|
+
|
77
|
+
#####################################
|
78
|
+
# Limit check, tag
|
79
|
+
def self.limits_tag(str)
|
80
|
+
Error.limits(str, 1, LimTagMax, LimTagInv, 'Tag')
|
81
|
+
end # def self.limits_tag()
|
82
|
+
|
83
|
+
|
84
|
+
# Maximum attachment name
|
85
|
+
LimAttachMax = 255
|
86
|
+
|
87
|
+
# Invalid attachment name characters
|
88
|
+
LimAttachInv = /[[:cntrl:]\/\\\*\?]/
|
89
|
+
|
90
|
+
#####################################
|
91
|
+
# Limit check, attachment name
|
92
|
+
def self.limits_attach(str)
|
93
|
+
Error.limits(str, 1, LimAttachMax, LimAttachInv, 'Attachment name')
|
94
|
+
end # def self.limits_attach()
|
95
|
+
|
96
|
+
|
97
|
+
#########################################################
|
98
|
+
# @!group Read attributes
|
99
|
+
|
100
|
+
|
101
|
+
#####################################
|
102
|
+
# Get entry item hash
|
103
|
+
#
|
104
|
+
# @return [String] The hash of the entry
|
105
|
+
# @raise (see #canonical)
|
106
|
+
def hash
|
107
|
+
if !@hash
|
108
|
+
@hash = Digest::SHA256.new.update(canonical).hexdigest
|
109
|
+
end
|
110
|
+
return @hash.dup
|
111
|
+
end # def hash
|
112
|
+
|
113
|
+
|
114
|
+
#####################################
|
115
|
+
# Generate canonical encoded string
|
116
|
+
#
|
117
|
+
# @return [String] Canonical output
|
118
|
+
# @raise [Error::Sanity] if the entry is not complete enought to
|
119
|
+
# generate canonical output.
|
120
|
+
def canonical
|
121
|
+
if !@canon
|
122
|
+
raise Error::Sanity, 'Entry not complete' if !@history
|
123
|
+
|
124
|
+
txt = "jckt %s\n" % @jacket
|
125
|
+
txt << "entr %d\n" % @entry
|
126
|
+
txt << "revn %d\n" % @revision
|
127
|
+
txt << "hist %d\n" % @history
|
128
|
+
txt << "amax %d\n" % @attach_max
|
129
|
+
txt << "time %s\n" % @time_str
|
130
|
+
txt << "titl %s\n" % @title
|
131
|
+
@tags.sort.each{ |tag| txt << "tags %s\n" % tag }
|
132
|
+
@attach.to_a.sort{|aa, bb| aa[0] <=> bb[0] }.each do |anum, ary|
|
133
|
+
txt << "atch %d %d %s\n" % [anum, ary[0], ary[1]]
|
134
|
+
end
|
135
|
+
txt << "\n"
|
136
|
+
txt << @body
|
137
|
+
@canon = txt
|
138
|
+
end
|
139
|
+
return @canon.dup
|
140
|
+
end # def canonical()
|
141
|
+
|
142
|
+
|
143
|
+
#####################################
|
144
|
+
# Generate JSON encoded string
|
145
|
+
#
|
146
|
+
# @return [String] JSON output
|
147
|
+
# @raise [Error::Sanity] if the entry is not complete enought to
|
148
|
+
# generate canonical output.
|
149
|
+
def json
|
150
|
+
if !@json
|
151
|
+
enc = {
|
152
|
+
'hash' => hash,
|
153
|
+
'jacket' => @jacket,
|
154
|
+
'entry' => @entry,
|
155
|
+
'revision' => @revision,
|
156
|
+
'history' => @history,
|
157
|
+
'max_attach' => @attach_max,
|
158
|
+
'time' => @time_str,
|
159
|
+
'title' => @title,
|
160
|
+
'tags' => @tags.sort,
|
161
|
+
'attachments' => @attach,
|
162
|
+
'body' => @body,
|
163
|
+
}
|
164
|
+
@json = JSON.generate(enc)
|
165
|
+
end
|
166
|
+
return @json.dup
|
167
|
+
end # def json
|
168
|
+
|
169
|
+
|
170
|
+
#####################################
|
171
|
+
# Get jacket
|
172
|
+
# @return [String, Boolean] The jacket hash ID or false if not set
|
173
|
+
def jacket
|
174
|
+
if @jacket
|
175
|
+
return @jacket.dup
|
176
|
+
else
|
177
|
+
return false
|
178
|
+
end
|
179
|
+
end # def jacket
|
180
|
+
|
181
|
+
|
182
|
+
#####################################
|
183
|
+
# Get entry number
|
184
|
+
#
|
185
|
+
# @return [Integer, Boolean] The entry number, or false if not set
|
186
|
+
def entry
|
187
|
+
if @entry
|
188
|
+
return @entry
|
189
|
+
else
|
190
|
+
return false
|
191
|
+
end
|
192
|
+
end # def entry
|
193
|
+
|
194
|
+
|
195
|
+
#####################################
|
196
|
+
# Get revision number
|
197
|
+
#
|
198
|
+
# @return [Integer, Boolean] The revision number, or false if not set
|
199
|
+
def revision
|
200
|
+
if @revision
|
201
|
+
return @revision
|
202
|
+
else
|
203
|
+
return false
|
204
|
+
end
|
205
|
+
end # def revision
|
206
|
+
|
207
|
+
|
208
|
+
#####################################
|
209
|
+
# Get history number
|
210
|
+
#
|
211
|
+
# @return [Integer, Boolean] The history number, or false if not set
|
212
|
+
def history
|
213
|
+
if @history
|
214
|
+
return @history
|
215
|
+
else
|
216
|
+
return false
|
217
|
+
end
|
218
|
+
end # def history
|
219
|
+
|
220
|
+
|
221
|
+
#####################################
|
222
|
+
# Get title
|
223
|
+
#
|
224
|
+
# @return [String, Boolean] Title of the entry, or false if not set
|
225
|
+
def title
|
226
|
+
if @title
|
227
|
+
return @title.dup
|
228
|
+
else
|
229
|
+
return false
|
230
|
+
end
|
231
|
+
end # def title
|
232
|
+
|
233
|
+
|
234
|
+
# Regex to parse time string
|
235
|
+
TimeStrReg = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/
|
236
|
+
private_constant :TimeStrReg
|
237
|
+
|
238
|
+
|
239
|
+
#####################################
|
240
|
+
# Get time
|
241
|
+
#
|
242
|
+
# @return [Time, Boolean] Time of the entry, or false if not set
|
243
|
+
def time
|
244
|
+
if !@time
|
245
|
+
return false if !@time_str
|
246
|
+
ma = TimeStrReg.match(@time_str)
|
247
|
+
raise Error::Limits, 'Invalid time string' if !ma
|
248
|
+
ary = ma[1,6].map{|str| str.to_i}
|
249
|
+
begin
|
250
|
+
@time = Time.utc(*ary)
|
251
|
+
rescue
|
252
|
+
raise Error::Limits, 'Invalid time string'
|
253
|
+
end
|
254
|
+
end
|
255
|
+
return @time.dup
|
256
|
+
end # def time
|
257
|
+
|
258
|
+
|
259
|
+
#####################################
|
260
|
+
# Get time string
|
261
|
+
#
|
262
|
+
# @return [String, Boolean] Encoded time string of the entry, or false
|
263
|
+
# if time not set
|
264
|
+
def time_str
|
265
|
+
if !@time_str
|
266
|
+
return false if !@time
|
267
|
+
@time_str = @time.strftime('%F %T')
|
268
|
+
end
|
269
|
+
return @time_str.dup
|
270
|
+
end # def time_str
|
271
|
+
|
272
|
+
|
273
|
+
#####################################
|
274
|
+
# Get body
|
275
|
+
#
|
276
|
+
# @return [String, Boolean] Entry body, or false if not set
|
277
|
+
def body
|
278
|
+
if @body
|
279
|
+
return @body.dup
|
280
|
+
else
|
281
|
+
return false
|
282
|
+
end
|
283
|
+
end # def body
|
284
|
+
|
285
|
+
|
286
|
+
#####################################
|
287
|
+
# Get tags
|
288
|
+
#
|
289
|
+
# @return [Array] of tag names
|
290
|
+
def tags
|
291
|
+
@tags = @tags.uniq
|
292
|
+
@tags.map{|tag| tag.dup }
|
293
|
+
end # def tags
|
294
|
+
|
295
|
+
|
296
|
+
#####################################
|
297
|
+
# Get permissions
|
298
|
+
def perms
|
299
|
+
@tags = @tags.uniq
|
300
|
+
@tags.select{|tag| tag.start_with?('perm: ')}.map{|tag| tag[6..-1]}
|
301
|
+
end # def perms
|
302
|
+
|
303
|
+
|
304
|
+
#####################################
|
305
|
+
# Get attached files
|
306
|
+
#
|
307
|
+
# @return [Array] of attachment information. Each entry is an array of
|
308
|
+
# \[attach_num, history_num, name\]
|
309
|
+
def attachments
|
310
|
+
res = []
|
311
|
+
@attach.each do |anum, ary|
|
312
|
+
res.push [anum, ary[0], ary[1].dup]
|
313
|
+
end
|
314
|
+
return res
|
315
|
+
end # def attach_each
|
316
|
+
|
317
|
+
|
318
|
+
#####################################
|
319
|
+
# Get max attachment
|
320
|
+
#
|
321
|
+
# @return [Integer, Boolean] The maximum attachment number, or false
|
322
|
+
# if not set
|
323
|
+
def attach_max
|
324
|
+
if @attach_max
|
325
|
+
return @attach_max
|
326
|
+
else
|
327
|
+
return false
|
328
|
+
end
|
329
|
+
end # def attach_max
|
330
|
+
|
331
|
+
|
332
|
+
|
333
|
+
#########################################################
|
334
|
+
# @!group Set attributes
|
335
|
+
|
336
|
+
|
337
|
+
#####################################
|
338
|
+
# Set entry using canonical encoding
|
339
|
+
#
|
340
|
+
# @param str [String] Canonical encoded entry
|
341
|
+
# @raise [Error::Corrupt] if encoding does not follow canonical rules
|
342
|
+
def canonical=(str)
|
343
|
+
@hash = nil
|
344
|
+
@canon = str.dup
|
345
|
+
lines = str.lines
|
346
|
+
|
347
|
+
ma = /^jckt ([0-9a-f]{64})$/.match lines.shift
|
348
|
+
raise(Error::Corrupt, 'Canonical entry jacket error') if !ma
|
349
|
+
@jacket = ma[1]
|
350
|
+
|
351
|
+
ma = /^entr (\d+)$/.match lines.shift
|
352
|
+
raise(Error::Corrupt, 'Canonical entry entry error') if !ma
|
353
|
+
@entry = ma[1].to_i
|
354
|
+
|
355
|
+
ma = /^revn (\d+)$/.match lines.shift
|
356
|
+
raise(Error::Corrupt, 'Canonical entry revision error') if !ma
|
357
|
+
@revision = ma[1].to_i
|
358
|
+
|
359
|
+
ma = /^hist (\d+)$/.match lines.shift
|
360
|
+
raise(Error::Corrupt, 'Canonical entry history error') if !ma
|
361
|
+
@history = ma[1].to_i
|
362
|
+
|
363
|
+
ma = /^amax (\d+)$/.match lines.shift
|
364
|
+
raise(Error::Corrupt, 'Caononical entry attach_max error') if !ma
|
365
|
+
@attach_max = ma[1].to_i
|
366
|
+
|
367
|
+
ma = /^time (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$/.match lines.shift
|
368
|
+
raise(Error::Corrupt, 'Canonical entry time error') if !ma
|
369
|
+
@time_str = ma[1]
|
370
|
+
@time = nil
|
371
|
+
|
372
|
+
ma = /^titl (.*)$/.match lines.shift
|
373
|
+
raise(Error::Corrupt, 'Canonical entry title error') if !ma
|
374
|
+
Entry.limits_title(ma[1])
|
375
|
+
@title = ma[1]
|
376
|
+
|
377
|
+
li = lines.shift
|
378
|
+
@tags = []
|
379
|
+
while( li && ma = /^tags (.+)$/.match(li) )
|
380
|
+
Entry.limits_tag(ma[1])
|
381
|
+
@tags.push ma[1]
|
382
|
+
li = lines.shift
|
383
|
+
end
|
384
|
+
|
385
|
+
@attach = {}
|
386
|
+
while( li && ma = /^atch (\d+) (\d+) (.+)$/.match(li) )
|
387
|
+
Entry.limits_attach(ma[3])
|
388
|
+
@attach[ma[1].to_i] = [ma[2].to_i, ma[3]]
|
389
|
+
li = lines.shift
|
390
|
+
end
|
391
|
+
|
392
|
+
unless li && li.strip.empty?
|
393
|
+
raise Error::Corrupt, 'Canonical entry body error'
|
394
|
+
end
|
395
|
+
|
396
|
+
txt = ''
|
397
|
+
lines.each{ |li| txt << li }
|
398
|
+
Entry.limits_body(txt)
|
399
|
+
@body = txt
|
400
|
+
|
401
|
+
_final
|
402
|
+
|
403
|
+
rescue
|
404
|
+
reset
|
405
|
+
raise
|
406
|
+
end # def canonical=()
|
407
|
+
|
408
|
+
|
409
|
+
#####################################
|
410
|
+
# Set jacket
|
411
|
+
#
|
412
|
+
# @param jck [String] The jacket hash ID
|
413
|
+
# @raise [Error::Limits] if jck is not a valid hash
|
414
|
+
# @raise [Error::Sanity] if changing an already set jacket hash ID
|
415
|
+
def jacket=(jck)
|
416
|
+
ma = /^([0-9a-f]{64})$/.match jck
|
417
|
+
raise(Error::Limits, 'Jacket hash not valid') if !ma
|
418
|
+
if @jacket
|
419
|
+
raise(Error::Sanity, 'Jacket already set') if @jacket != jck
|
420
|
+
else
|
421
|
+
@jacket = jck.dup
|
422
|
+
_change
|
423
|
+
end
|
424
|
+
end # def jacket=()
|
425
|
+
|
426
|
+
|
427
|
+
#####################################
|
428
|
+
# Set entry number
|
429
|
+
# @param enum [Integer] The entry number
|
430
|
+
# @raise [ArgumentError] if enum is negative
|
431
|
+
# @raise [Error::Sanity] if changing an already set entry number
|
432
|
+
def entry=(enum)
|
433
|
+
raise(ArgumentError, 'Entry number invalid') if enum < 0
|
434
|
+
if @entry
|
435
|
+
raise(Error::Sanity, 'Changing entry number') if @entry != enum
|
436
|
+
else
|
437
|
+
@entry = enum
|
438
|
+
_change
|
439
|
+
end
|
440
|
+
end # def entry=()
|
441
|
+
|
442
|
+
|
443
|
+
#####################################
|
444
|
+
# Set title
|
445
|
+
#
|
446
|
+
# @param ttl [String] Title of the entry
|
447
|
+
# @raise [Error::Limits] if ttl exceeds allowed values
|
448
|
+
def title=(ttl)
|
449
|
+
Entry.limits_title(ttl)
|
450
|
+
@title = ttl.dup
|
451
|
+
_change
|
452
|
+
end # def title=()
|
453
|
+
|
454
|
+
|
455
|
+
#####################################
|
456
|
+
# Set time
|
457
|
+
#
|
458
|
+
# @param tme [Time] Time of the entry
|
459
|
+
def time=(tme)
|
460
|
+
@time = tme.utc
|
461
|
+
@time_str = nil
|
462
|
+
end # def time=()
|
463
|
+
|
464
|
+
|
465
|
+
#####################################
|
466
|
+
# Set encoded time string
|
467
|
+
#
|
468
|
+
# @param tme [String] Encoded time string
|
469
|
+
# @raise [Error::Limits] if tme is not properly written
|
470
|
+
def time_str=(tme)
|
471
|
+
@time = nil
|
472
|
+
@time_str = tme.dup
|
473
|
+
time
|
474
|
+
end # def time_str=()
|
475
|
+
|
476
|
+
|
477
|
+
#####################################
|
478
|
+
# Set body
|
479
|
+
#
|
480
|
+
# @param bdy [String] Entry body
|
481
|
+
# @raise [Error::Limits] if bdy exceeds allowed values
|
482
|
+
def body=(bdy)
|
483
|
+
Entry.limits_body(bdy)
|
484
|
+
@body = bdy.dup
|
485
|
+
_change
|
486
|
+
end # def body=()
|
487
|
+
|
488
|
+
|
489
|
+
#########################################################
|
490
|
+
# @!group General interface
|
491
|
+
|
492
|
+
|
493
|
+
#####################################
|
494
|
+
# Create a new entry
|
495
|
+
def initialize
|
496
|
+
reset
|
497
|
+
end # def initialize
|
498
|
+
|
499
|
+
|
500
|
+
#####################################
|
501
|
+
# Reset to blank entry
|
502
|
+
#
|
503
|
+
# @return [Entry] self
|
504
|
+
def reset()
|
505
|
+
@hash = nil
|
506
|
+
@canon = nil
|
507
|
+
@jacket = nil
|
508
|
+
@entry = nil
|
509
|
+
@revision = 1
|
510
|
+
@history = nil
|
511
|
+
@title = nil
|
512
|
+
@time = nil
|
513
|
+
@time_str = nil
|
514
|
+
@body = nil
|
515
|
+
@tags = []
|
516
|
+
@attach = {}
|
517
|
+
@attach_file = {}
|
518
|
+
@attach_max = 0
|
519
|
+
|
520
|
+
@time_old = nil
|
521
|
+
@tags_old = []
|
522
|
+
|
523
|
+
return self
|
524
|
+
end # def reset()
|
525
|
+
|
526
|
+
|
527
|
+
#####################################
|
528
|
+
# Update an entry
|
529
|
+
#
|
530
|
+
# @note If time has not been set, it defaults to the current time
|
531
|
+
#
|
532
|
+
# The changes returned include:
|
533
|
+
# * :time - true if time changed
|
534
|
+
# * :tags_add - list of new tags
|
535
|
+
# * :tags_del - list of tags deleted
|
536
|
+
# * :files - hash of attachment number => \[file, hash\]
|
537
|
+
#
|
538
|
+
# @param hnum [Integer] History number
|
539
|
+
# @raise [Error::Sanity] if no changes have been made to an entry
|
540
|
+
# @raise [Error::Sanity] if entry does not have at least jacket,
|
541
|
+
# entry, title, and body set
|
542
|
+
# @return [Hash] Describing changes
|
543
|
+
def update(hnum)
|
544
|
+
raise Error::Sanity, 'Update entry with no changes' if @history
|
545
|
+
if !@jacket && !@entry && !@title && !@body
|
546
|
+
raise Error::Sanity, 'Update incomplete entry'
|
547
|
+
end
|
548
|
+
|
549
|
+
@history = hnum
|
550
|
+
change = {}
|
551
|
+
|
552
|
+
if !@time_str
|
553
|
+
@time = Time.new.utc if !@time
|
554
|
+
@time_str = @time.strftime('%F %T')
|
555
|
+
end
|
556
|
+
change[:time] = @time_str != @time_old
|
557
|
+
|
558
|
+
@tags = @tags.uniq
|
559
|
+
change[:tags_add] = @tags - @tags_old
|
560
|
+
change[:tags_del] = @tags_old - @tags
|
561
|
+
|
562
|
+
@attach.each{ |anum, ary| ary[0] = hnum if ary[0] == 0 }
|
563
|
+
change[:files] = @attach_file
|
564
|
+
|
565
|
+
_final()
|
566
|
+
return change
|
567
|
+
end # def update()
|
568
|
+
|
569
|
+
|
570
|
+
#########################################################
|
571
|
+
# @!group Edit tags and attached files
|
572
|
+
|
573
|
+
|
574
|
+
#####################################
|
575
|
+
# Rename an attachment
|
576
|
+
#
|
577
|
+
# @param anum [Integer] Attachment number to rename
|
578
|
+
# @param name [String] New attachment name
|
579
|
+
# @raise [Error::Sanity] if attachment does not exist
|
580
|
+
# @raise [Error::Limits] if name exceeds allowed values
|
581
|
+
def rename(anum, name)
|
582
|
+
raise(Error::Sanity, 'Non-existent attachment') if !@attach[anum]
|
583
|
+
Entry.limits_attach(name)
|
584
|
+
@attach[anum][1] = name.dup
|
585
|
+
_change()
|
586
|
+
end # def rename()
|
587
|
+
|
588
|
+
|
589
|
+
#####################################
|
590
|
+
# Delete an attachment
|
591
|
+
#
|
592
|
+
# @param anum [Integer] anum Attachment number to delete
|
593
|
+
# @raise [Error::Sanity] if attachment does not exist
|
594
|
+
def delete(anum)
|
595
|
+
raise(Error::Sanity, 'Non-existent attachment') if !@attach[anum]
|
596
|
+
@attach.delete(anum)
|
597
|
+
_change()
|
598
|
+
end # def delete()
|
599
|
+
|
600
|
+
|
601
|
+
#####################################
|
602
|
+
# Replace an attachment
|
603
|
+
#
|
604
|
+
# @note If hash is not provided, it will be calculated. This can take
|
605
|
+
# a long time for large files.
|
606
|
+
#
|
607
|
+
# @param anum [Integer] Attachment number to replace
|
608
|
+
# @param file [File] Temporary file to attach
|
609
|
+
# @param hash [String] The SHA256 hash of the file
|
610
|
+
# @raise [Error::Sanity] if attachment does not exist
|
611
|
+
def replace(anum, file, hash=nil)
|
612
|
+
raise(Error::Sanity, 'Non-existent attachment') if !@attach[anum]
|
613
|
+
hsto = hash ? hash.dup : Digest::SHA256.file(file.path).hexdigest
|
614
|
+
@attach[anum][0] = 0
|
615
|
+
@attach_file[anum] = [file, hsto]
|
616
|
+
_change()
|
617
|
+
end # def replace()
|
618
|
+
|
619
|
+
|
620
|
+
#####################################
|
621
|
+
# Add an attachment
|
622
|
+
#
|
623
|
+
# @note (see #replace)
|
624
|
+
#
|
625
|
+
# @param name [String] Attachment name
|
626
|
+
# @param file [File] Temporary file to attach
|
627
|
+
# @param hash [String] The SHA256 hash of the file
|
628
|
+
# @raise [Error::Limits] if name exceeds allowed values
|
629
|
+
def attach(name, file, hash=nil)
|
630
|
+
Entry.limits_attach(name)
|
631
|
+
hsto = hash ? hash.dup : Digest::SHA256.file(file.path).hexdigest
|
632
|
+
@attach_max += 1
|
633
|
+
@attach[@attach_max] = [0, name.dup]
|
634
|
+
@attach_file[@attach_max] = [file, hsto]
|
635
|
+
_change()
|
636
|
+
end # def attach()
|
637
|
+
|
638
|
+
|
639
|
+
#####################################
|
640
|
+
# Set tag
|
641
|
+
#
|
642
|
+
# @param tnam [String] Tag name to set
|
643
|
+
# @raise [Error::Limits] if tnam exceeds allowed values
|
644
|
+
def tag(tnam)
|
645
|
+
name = _tag_normalize(tnam)
|
646
|
+
@tags.push name
|
647
|
+
_change()
|
648
|
+
end # def tag()
|
649
|
+
|
650
|
+
|
651
|
+
#####################################
|
652
|
+
# Clear tag
|
653
|
+
#
|
654
|
+
# @param tnam [String] tnam Tag name to clear
|
655
|
+
# @raise [Error::Limits] if tnam exceeds allowed values
|
656
|
+
def untag(tnam)
|
657
|
+
name = _tag_normalize(tnam)
|
658
|
+
@tags.delete name
|
659
|
+
_change()
|
660
|
+
end # def untag()
|
661
|
+
|
662
|
+
|
663
|
+
private
|
664
|
+
|
665
|
+
# Change to entry
|
666
|
+
def _change()
|
667
|
+
@revision = @revision + 1 if @history
|
668
|
+
@history = nil
|
669
|
+
@hash = nil
|
670
|
+
@canon = nil
|
671
|
+
end # def _change()
|
672
|
+
|
673
|
+
|
674
|
+
# Finalize entry
|
675
|
+
def _final
|
676
|
+
@time_old = @time_str ? @time_str.dup : nil
|
677
|
+
@tags_old = @tags.map{|tg| tg.dup }
|
678
|
+
@attach_file = {}
|
679
|
+
end # def _final()
|
680
|
+
|
681
|
+
|
682
|
+
# Normalize tag name
|
683
|
+
def _tag_normalize(tnam)
|
684
|
+
idx = tnam.index(':')
|
685
|
+
if idx
|
686
|
+
pre = tnam[0, idx].strip
|
687
|
+
post = tnam[idx+1..-1].strip
|
688
|
+
return pre + ': ' + post
|
689
|
+
else
|
690
|
+
return tnam.strip
|
691
|
+
end
|
692
|
+
end # def _tag_normalize()
|
693
|
+
|
694
|
+
|
695
|
+
end # class Entry
|
696
|
+
|
697
|
+
end # module Sgfa
|