sgfa 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/data/sgfa_web.css +37 -34
- data/lib/sgfa/binder.rb +205 -3
- data/lib/sgfa/binder_fs.rb +55 -20
- data/lib/sgfa/cli/binder.rb +248 -31
- data/lib/sgfa/cli/jacket.rb +206 -0
- data/lib/sgfa/entry.rb +96 -0
- data/lib/sgfa/error.rb +2 -2
- data/lib/sgfa/jacket.rb +314 -9
- data/lib/sgfa/jacket_fs.rb +18 -4
- data/lib/sgfa/state_fs.rb +33 -9
- data/lib/sgfa/store_s3.rb +119 -0
- data/lib/sgfa/web/base.rb +7 -0
- data/lib/sgfa/web/binder.rb +294 -85
- metadata +4 -3
data/lib/sgfa/jacket_fs.rb
CHANGED
@@ -31,14 +31,28 @@ class JacketFs < Jacket
|
|
31
31
|
#
|
32
32
|
# @param path [String] Path to create the jacket
|
33
33
|
# @param id_text [String] Text ID of the jacket
|
34
|
-
# @return [String] Hash ID of the jacket
|
35
34
|
# @raise [Error::Limits] if id_text exceeds allowed limits
|
36
35
|
# @raise [Error::Conflict] if path already exists
|
36
|
+
# @return [String] Hash ID of the jacket
|
37
37
|
def self.create(path, id_text)
|
38
|
+
id_hash = Digest::SHA256.new.update(id_text).hexdigest
|
39
|
+
JacketFs.create_raw(path, id_text, id_hash)
|
40
|
+
end # def self.create()
|
41
|
+
|
42
|
+
|
43
|
+
#####################################
|
44
|
+
# Create a jacket from backup
|
45
|
+
#
|
46
|
+
# @param path (see create)
|
47
|
+
# @param id_text (see create)
|
48
|
+
# @param id_hash [String] Hash ID of the jacket
|
49
|
+
# @return [String] Hash ID of the jacket
|
50
|
+
# @raise [Error::Limits] if id_text exceeds allowed limits
|
51
|
+
# @raise [Error::Conflict] if path already exists
|
52
|
+
# @note You probably want to use {create}, not this method
|
53
|
+
def self.create_raw(path, id_text, id_hash)
|
38
54
|
Jacket.limits_id(id_text)
|
39
55
|
|
40
|
-
# info
|
41
|
-
id_hash = Digest::SHA256.new.update(id_text).hexdigest
|
42
56
|
info = {
|
43
57
|
'sgfa_jacket_ver' => 1,
|
44
58
|
'id_hash' => id_hash,
|
@@ -60,7 +74,7 @@ class JacketFs < Jacket
|
|
60
74
|
StoreFs.create(File.join(path, 'store'))
|
61
75
|
|
62
76
|
return id_hash
|
63
|
-
end # def self.
|
77
|
+
end # def self.create_raw()
|
64
78
|
|
65
79
|
|
66
80
|
#####################################
|
data/lib/sgfa/state_fs.rb
CHANGED
@@ -190,6 +190,15 @@ class StateFs
|
|
190
190
|
# @raise [Error::Sanity] if state not open
|
191
191
|
# @raise [Error::Corrupt] if tag list is missing
|
192
192
|
def list
|
193
|
+
tagh, max = _list
|
194
|
+
return tagh.keys
|
195
|
+
end # def list
|
196
|
+
|
197
|
+
|
198
|
+
#####################################
|
199
|
+
# Read raw tag list
|
200
|
+
# return [Array] \[max_tagn, tag_hash\]
|
201
|
+
def _list
|
193
202
|
raise Error::Sanity, 'State not open' if !@path
|
194
203
|
|
195
204
|
ftn = File.join(@path, TagList)
|
@@ -198,8 +207,20 @@ class StateFs
|
|
198
207
|
rescue Errno::ENOENT
|
199
208
|
raise Error::Corrupt, 'Unable to read tag list'
|
200
209
|
end
|
201
|
-
|
202
|
-
|
210
|
+
|
211
|
+
tagh = {}
|
212
|
+
max = 0
|
213
|
+
txt.lines.each do |ln|
|
214
|
+
ma = /^(\d{9}) (.*)$/.match(ln)
|
215
|
+
raise Error::Corrupt, 'Tag list format incorrect' if !ma
|
216
|
+
num = ma[1].to_i
|
217
|
+
tagh[ma[2]] = num
|
218
|
+
max = num if num > max
|
219
|
+
end
|
220
|
+
|
221
|
+
return tagh, max
|
222
|
+
end # _list
|
223
|
+
private :_list
|
203
224
|
|
204
225
|
|
205
226
|
#####################################
|
@@ -216,11 +237,13 @@ class StateFs
|
|
216
237
|
def tag(name, offs, max)
|
217
238
|
raise Error::Sanity, 'State not open' if !@path
|
218
239
|
|
219
|
-
|
240
|
+
tagh, tagm = _list
|
241
|
+
raise Error::NonExistent, 'Tag does not exist' if !tagh[name]
|
242
|
+
fn = File.join(@path, tagh[name].to_s)
|
220
243
|
begin
|
221
244
|
fi = File.open(fn, 'rb')
|
222
245
|
rescue Errno::ENOENT
|
223
|
-
raise Error::
|
246
|
+
raise Error::Corrupt, 'Tag file missing'
|
224
247
|
end
|
225
248
|
|
226
249
|
ents = []
|
@@ -257,8 +280,7 @@ class StateFs
|
|
257
280
|
|
258
281
|
# read list of tags
|
259
282
|
changed = false
|
260
|
-
thash =
|
261
|
-
self.list.each{|tag| thash[tag] = true }
|
283
|
+
thash, max = _list
|
262
284
|
|
263
285
|
cng.each do |tag, hc|
|
264
286
|
cnt = 0
|
@@ -267,14 +289,15 @@ class StateFs
|
|
267
289
|
se = hc.to_a.select{|en, ti| ti }.sort{|aa, bb| aa[1] <=> bb[1] }
|
268
290
|
|
269
291
|
# files
|
270
|
-
fn = File.join(@path, tag)
|
271
292
|
if thash[tag]
|
293
|
+
fn = File.join(@path, thash[tag].to_s)
|
272
294
|
begin
|
273
295
|
oldf = File.open(fn, 'rb')
|
274
296
|
rescue Errno::ENOENT
|
275
297
|
raise Error::Corrupt, 'Existing tag is missing'
|
276
298
|
end
|
277
299
|
else
|
300
|
+
fn = File.join(@path, (max + 1).to_s)
|
278
301
|
oldf = nil
|
279
302
|
end
|
280
303
|
newf = Tempfile.new('state', @path, :encoding => 'ASCII-8BIT')
|
@@ -319,7 +342,8 @@ class StateFs
|
|
319
342
|
else
|
320
343
|
FileUtils.ln(newf.path, fn, :force => true)
|
321
344
|
if !oldf
|
322
|
-
|
345
|
+
max += 1
|
346
|
+
thash[tag] = max
|
323
347
|
changed = true
|
324
348
|
end
|
325
349
|
end
|
@@ -330,7 +354,7 @@ class StateFs
|
|
330
354
|
if changed
|
331
355
|
fnl = File.join(@path, TagList)
|
332
356
|
File.open(fnl, 'w', :encoding => 'utf-8') do |fi|
|
333
|
-
thash.
|
357
|
+
thash.each{|tag, num| fi.puts '%09d %s' % [num, tag] }
|
334
358
|
end
|
335
359
|
end
|
336
360
|
|
@@ -0,0 +1,119 @@
|
|
1
|
+
#
|
2
|
+
# Simple Group of Filing Applications
|
3
|
+
# Jacket store using AWS S3.
|
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 'aws-sdk'
|
13
|
+
require 'tempfile'
|
14
|
+
|
15
|
+
require_relative 'error'
|
16
|
+
|
17
|
+
module Sgfa
|
18
|
+
|
19
|
+
#####################################################################
|
20
|
+
# Stores copies of {History}, {Entry}, and attached files that are
|
21
|
+
# in a {Jacket} using AWS S3.
|
22
|
+
#
|
23
|
+
class StoreS3
|
24
|
+
|
25
|
+
#####################################
|
26
|
+
# Open the store
|
27
|
+
#
|
28
|
+
# @param client [AWS::S3::Client] The configured S3 client
|
29
|
+
# @param bucket [String] The bucket name
|
30
|
+
# @param prefix [String] Prefix to use for object keys
|
31
|
+
def open(client, bucket, prefix=nil)
|
32
|
+
@s3 = client
|
33
|
+
@bck = bucket
|
34
|
+
@pre = prefix || ''
|
35
|
+
end # def open()
|
36
|
+
|
37
|
+
|
38
|
+
#####################################
|
39
|
+
# Close the store
|
40
|
+
def close()
|
41
|
+
true
|
42
|
+
end # def close()
|
43
|
+
|
44
|
+
|
45
|
+
#####################################
|
46
|
+
# Get a temp file
|
47
|
+
def temp
|
48
|
+
Tempfile.new('blob', Dir.tmpdir, :encoding => 'utf-8')
|
49
|
+
end # def temp
|
50
|
+
|
51
|
+
|
52
|
+
#####################################
|
53
|
+
# Get key
|
54
|
+
def _key(type, item)
|
55
|
+
case type
|
56
|
+
when :entry then ext = '-e'
|
57
|
+
when :history then ext = '-h'
|
58
|
+
when :file then ext = '-f'
|
59
|
+
else raise NotImplementedError, 'Invalid item type'
|
60
|
+
end
|
61
|
+
key = @pre + item + ext
|
62
|
+
return key
|
63
|
+
end # def _key()
|
64
|
+
private :_key
|
65
|
+
|
66
|
+
|
67
|
+
#####################################
|
68
|
+
# Read an item from the store
|
69
|
+
def read(type, item)
|
70
|
+
key = _key(type, item)
|
71
|
+
fi = temp
|
72
|
+
fi.set_encoding(Encoding::ASCII_8BIT)
|
73
|
+
@s3.get_object( bucket: @bck, key: key, response_target: fi )
|
74
|
+
fi.rewind
|
75
|
+
return fi
|
76
|
+
rescue Aws::S3::Errors::NoSuchKey
|
77
|
+
return false
|
78
|
+
end # def read()
|
79
|
+
|
80
|
+
|
81
|
+
#####################################
|
82
|
+
# Store an item
|
83
|
+
def write(type, item, cont)
|
84
|
+
key = _key(type, item)
|
85
|
+
cont.rewind
|
86
|
+
@s3.put_object( bucket: @bck, key: key, body: cont )
|
87
|
+
|
88
|
+
if cont.respond_to?( :close! )
|
89
|
+
cont.close!
|
90
|
+
else
|
91
|
+
cont.close
|
92
|
+
end
|
93
|
+
end # def write()
|
94
|
+
|
95
|
+
|
96
|
+
#####################################
|
97
|
+
# Delete
|
98
|
+
def delete(type, item)
|
99
|
+
key = _key(type, item)
|
100
|
+
@s3.delete_object( bucket: @bck, key: key )
|
101
|
+
return true
|
102
|
+
rescue Aws::S3::Errors::NoSuchKey
|
103
|
+
return false
|
104
|
+
end # def delete()
|
105
|
+
|
106
|
+
|
107
|
+
#####################################
|
108
|
+
# Get size of an item
|
109
|
+
def size(type, item)
|
110
|
+
key = _key(type, item)
|
111
|
+
resp = @s3.head_object( bucket: @bck, key: key )
|
112
|
+
return resp.content_length
|
113
|
+
rescue Aws::S3::Errors::NotFound
|
114
|
+
return false
|
115
|
+
end # def size()
|
116
|
+
|
117
|
+
end # class StoreS3
|
118
|
+
|
119
|
+
end # module Sgfa
|
data/lib/sgfa/web/base.rb
CHANGED
@@ -169,6 +169,13 @@ class Base
|
|
169
169
|
end # def _escape()
|
170
170
|
|
171
171
|
|
172
|
+
#####################################
|
173
|
+
# Escape URL using only percent encoding
|
174
|
+
def _escape_path(txt)
|
175
|
+
Rack::Utils.escape_path(txt)
|
176
|
+
end # def _escape_path():
|
177
|
+
|
178
|
+
|
172
179
|
##########################################
|
173
180
|
# Unescape URL
|
174
181
|
def _escape_un(txt)
|
data/lib/sgfa/web/binder.rb
CHANGED
@@ -22,7 +22,6 @@ module Web
|
|
22
22
|
#####################################################################
|
23
23
|
# Binder web interface
|
24
24
|
#
|
25
|
-
# @todo Add a docket view
|
26
25
|
class Binder < Base
|
27
26
|
|
28
27
|
#####################################
|
@@ -139,6 +138,7 @@ class Binder < Base
|
|
139
138
|
when '_log'; return _get_log(env, path)
|
140
139
|
when '_list'; return _get_list(env, path)
|
141
140
|
when '_info'; return _get_info(env, path)
|
141
|
+
when '_docket'; return _get_docket(env, path)
|
142
142
|
else; return
|
143
143
|
end
|
144
144
|
|
@@ -170,6 +170,7 @@ class Binder < Base
|
|
170
170
|
|
171
171
|
NavBarJacket = [
|
172
172
|
['Tag', '_tag'],
|
173
|
+
['Docket', '_docket'],
|
173
174
|
['List', '_list'],
|
174
175
|
['Entry', nil],
|
175
176
|
['Edit', '_edit'],
|
@@ -262,6 +263,18 @@ class Binder < Base
|
|
262
263
|
end # def _link_tag()
|
263
264
|
|
264
265
|
|
266
|
+
#####################################
|
267
|
+
# Link to a docket
|
268
|
+
def _link_dock(env, tag, disp)
|
269
|
+
"<a href='%s/%s/_docket/%s'>%s</a>" % [
|
270
|
+
env['SCRIPT_NAME'],
|
271
|
+
env['sgfa.jacket.url'],
|
272
|
+
_escape(tag),
|
273
|
+
disp
|
274
|
+
]
|
275
|
+
end # def _link_dock()
|
276
|
+
|
277
|
+
|
265
278
|
#####################################
|
266
279
|
# Link to a tag prefix
|
267
280
|
def _link_prefix(env, pre, disp)
|
@@ -283,7 +296,7 @@ class Binder < Base
|
|
283
296
|
enum,
|
284
297
|
anum,
|
285
298
|
hnum,
|
286
|
-
|
299
|
+
_escape_path(name),
|
287
300
|
disp
|
288
301
|
]
|
289
302
|
end # def _link_attach()
|
@@ -300,7 +313,7 @@ class Binder < Base
|
|
300
313
|
|
301
314
|
JacketsForm =
|
302
315
|
"\n<hr>\n<form class='edit' method='post' action='%s' " +
|
303
|
-
"enctype='multipart/form-data'>\n" +
|
316
|
+
"enctype='multipart/form-data' accept-charset='utf-8'>\n" +
|
304
317
|
"<fieldset><legend>Create or Edit Jacket</legend>\n" +
|
305
318
|
"<label for='jacket'>Name:</label>" +
|
306
319
|
"<input class='jacket' name='jacket' type='text'><br>\n" +
|
@@ -389,7 +402,7 @@ class Binder < Base
|
|
389
402
|
|
390
403
|
UsersForm =
|
391
404
|
"\n<hr>\n<form class='edit' method='post' action='%s' " +
|
392
|
-
"enctype='multipart/form-data'>\n" +
|
405
|
+
"enctype='multipart/form-data' accept-charset='utf-8'>\n" +
|
393
406
|
"<fieldset><legend>Set User or Group Permissions</legend>\n" +
|
394
407
|
"<label for='user'>Name:</label>" +
|
395
408
|
"<input class='user' name='user' type='text'><br>\n" +
|
@@ -490,7 +503,7 @@ class Binder < Base
|
|
490
503
|
|
491
504
|
|
492
505
|
TagTable =
|
493
|
-
"<div class='
|
506
|
+
"<div class='tagname'>Tag: %s</div>\n" +
|
494
507
|
"<table class='list'>\n<tr>" +
|
495
508
|
"<th>Time</th><th>Title</th><th>Files</th><th>Tags</th><th>Edit</th>" +
|
496
509
|
"</tr>\n%s</table>\n"
|
@@ -528,7 +541,7 @@ class Binder < Base
|
|
528
541
|
html = 'No entries'
|
529
542
|
else
|
530
543
|
rows = ''
|
531
|
-
ents.reverse_each do |enum, rnum, time, title, tcnt, acnt|
|
544
|
+
ents.reverse_each do |enum, rnum, hnum, time, title, tcnt, acnt|
|
532
545
|
rows << TagRow % [
|
533
546
|
time.localtime.strftime("%F %T %z"),
|
534
547
|
_link_entry(env, enum, _escape_html(title)),
|
@@ -545,13 +558,87 @@ class Binder < Base
|
|
545
558
|
_escape(tag)
|
546
559
|
]
|
547
560
|
query = (per != PageSize) ? { 'perpage' => per.to_s } : nil
|
548
|
-
pages = _link_pages(page, per, size, link, query)
|
561
|
+
pages = PageDiv % _link_pages(page, per, size, link, query)
|
549
562
|
|
550
563
|
env['sgfa.status'] = :ok
|
551
564
|
env['sgfa.html'] = html + pages
|
552
565
|
end # def _get_tag()
|
553
566
|
|
554
567
|
|
568
|
+
# each entry
|
569
|
+
DocketEach =
|
570
|
+
"<div class='title'>%s</div>
|
571
|
+
<div class='body'><em>Permission denied</em></div>
|
572
|
+
<div class='sidebar'>
|
573
|
+
<div class='time'>%s</div>
|
574
|
+
<div class='history'>Revision: %d %s<br>History: %s</div>
|
575
|
+
<div class='tags'>Number of tags: %d</div>
|
576
|
+
<div class='attach'>Number of attachments: %d</div>
|
577
|
+
</div>
|
578
|
+
<div class='hash'>Hash: Permission denied</div>
|
579
|
+
"
|
580
|
+
|
581
|
+
# Page division
|
582
|
+
PageDiv = "<div class='pages'>%s</div>\n"
|
583
|
+
|
584
|
+
|
585
|
+
#####################################
|
586
|
+
# Get a docket view
|
587
|
+
def _get_docket(env, path)
|
588
|
+
_navbar_jacket(env, 'Docket')
|
589
|
+
|
590
|
+
# tag, page, perpage
|
591
|
+
tag = path.empty? ? Jacket::TagAll : _escape_un(path.shift)
|
592
|
+
page = path.empty? ? 1 : path.shift.to_i
|
593
|
+
page = 1 if page == 0
|
594
|
+
rck = Rack::Request.new(env)
|
595
|
+
params = rck.GET
|
596
|
+
per = params['perpage'] ? params['perpage'].to_i : 0
|
597
|
+
per = PageSize if( per == 0 || per > PageSizeMax )
|
598
|
+
|
599
|
+
# get
|
600
|
+
tr = _trans(env)
|
601
|
+
size, ents = env['sgfa.binder'].read_tag(tr, tag, (page-1)*per, per,
|
602
|
+
raw: true)
|
603
|
+
html = "<div class='tagname'>Docket: %s</div>\n" % _escape_html(tag)
|
604
|
+
if ents.size == 0
|
605
|
+
html << 'No entries'
|
606
|
+
else
|
607
|
+
rows = ''
|
608
|
+
ents.reverse_each do |item|
|
609
|
+
if item.is_a?(Entry)
|
610
|
+
rows << _disp_entry(env, item, jacket: false, current: true)
|
611
|
+
else
|
612
|
+
enum, rnum, hnum, time, title, tcnt, acnt = item
|
613
|
+
if rnum == 1
|
614
|
+
prev = 'previous'
|
615
|
+
else
|
616
|
+
prev = _link_revision(env, enum, rnum-1, 'previous')
|
617
|
+
end
|
618
|
+
hist = _link_history(env, hnum, hnum.to_s)
|
619
|
+
rows << DocketEach % [
|
620
|
+
_escape_html(title),
|
621
|
+
time.localtime.strftime("%F %T %z"),
|
622
|
+
rnum, prev, hist, tcnt, acnt,
|
623
|
+
]
|
624
|
+
end
|
625
|
+
end
|
626
|
+
html << rows
|
627
|
+
end
|
628
|
+
|
629
|
+
link = '%s/%s/_docket/%s' % [
|
630
|
+
env['SCRIPT_NAME'],
|
631
|
+
env['sgfa.jacket.url'],
|
632
|
+
_escape(tag)
|
633
|
+
]
|
634
|
+
query = (per != PageSize) ? { 'perpage' => per.to_s } : nil
|
635
|
+
pages = PageDiv % _link_pages(page, per, size, link, query)
|
636
|
+
|
637
|
+
env['sgfa.status'] = :ok
|
638
|
+
env['sgfa.html'] = html + pages
|
639
|
+
end # def _get_docket()
|
640
|
+
|
641
|
+
|
555
642
|
LogTable =
|
556
643
|
"<table class='list'>\n<tr>" +
|
557
644
|
"<th>History</th><th>Date/Time</th><th>User</th><th>Entries</th>" +
|
@@ -628,11 +715,11 @@ class Binder < Base
|
|
628
715
|
"<div class='tags'>%s</div>\n" +
|
629
716
|
"<div class='attach'>%s</div>\n" +
|
630
717
|
"</div>\n" +
|
631
|
-
"<div class='hash'>Hash: %s
|
718
|
+
"<div class='hash'>Hash: %s</div>\n"
|
632
719
|
|
633
720
|
#####################################
|
634
721
|
# Display an entry
|
635
|
-
def _disp_entry(env, ent)
|
722
|
+
def _disp_entry(env, ent, opts={})
|
636
723
|
|
637
724
|
enum = ent.entry
|
638
725
|
rnum = ent.revision
|
@@ -643,7 +730,7 @@ class Binder < Base
|
|
643
730
|
if tl.empty?
|
644
731
|
tags << "none\n"
|
645
732
|
else
|
646
|
-
tl.each do |tag|
|
733
|
+
tl.sort.each do |tag|
|
647
734
|
tags << _link_tag(env, tag, _escape_html(tag)) + "<br>\n"
|
648
735
|
end
|
649
736
|
end
|
@@ -663,17 +750,20 @@ class Binder < Base
|
|
663
750
|
else
|
664
751
|
prev = _link_revision(env, enum, rnum-1, 'previous')
|
665
752
|
end
|
666
|
-
curr = _link_entry(env, enum, 'current')
|
753
|
+
curr = opts[:current] ? '' : _link_entry(env, enum, 'current')
|
667
754
|
edit = _link_edit(env, enum, 'edit')
|
755
|
+
hist = _link_history(env, hnum, hnum.to_s)
|
756
|
+
hash = ent.hash
|
757
|
+
hash << ('<br>Jacket: %s' % ent.jacket) if opts[:jacket]
|
668
758
|
|
669
759
|
body = EntryDisp % [
|
670
760
|
_escape_html(ent.title),
|
671
761
|
_escape_html(ent.body),
|
672
762
|
ent.time.localtime.strftime('%F %T %z'),
|
673
|
-
rnum, prev, curr,
|
763
|
+
rnum, prev, curr, hist, edit,
|
674
764
|
tags,
|
675
765
|
att,
|
676
|
-
|
766
|
+
hash
|
677
767
|
]
|
678
768
|
|
679
769
|
return body
|
@@ -735,14 +825,14 @@ class Binder < Base
|
|
735
825
|
|
736
826
|
ListTable =
|
737
827
|
"<table class='list'>\n<tr>" +
|
738
|
-
"<th>Tag</th><th>Number</th></tr>\n" +
|
828
|
+
"<th>Tag</th><th>Number</th><th>View</th></tr>\n" +
|
739
829
|
"%s\n</table>\n"
|
740
830
|
|
741
831
|
ListPrefix =
|
742
|
-
"<tr><td class='prefix'>%s: prefix</td><td>%d tags</td></tr>\n"
|
832
|
+
"<tr><td class='prefix'>%s: prefix</td><td>%d tags</td><td></td></tr>\n"
|
743
833
|
|
744
834
|
ListTag =
|
745
|
-
"<tr><td class='tag'>%s</td><td>%d entries</td></tr>\n"
|
835
|
+
"<tr><td class='tag'>%s</td><td>%d entries</td><td>%s %s</td></tr>\n"
|
746
836
|
|
747
837
|
#####################################
|
748
838
|
# Get list of tags
|
@@ -780,8 +870,12 @@ class Binder < Base
|
|
780
870
|
end
|
781
871
|
regular.sort.each do |tag|
|
782
872
|
size, ents = bnd.read_tag(tr, tag, 0, 0)
|
783
|
-
rows << ListTag %
|
784
|
-
|
873
|
+
rows << ListTag % [
|
874
|
+
_link_tag(env, tag, _escape_html(tag)),
|
875
|
+
size,
|
876
|
+
_link_tag(env, tag, 'Tag'),
|
877
|
+
_link_dock(env, tag, 'Docket')
|
878
|
+
]
|
785
879
|
end
|
786
880
|
|
787
881
|
# list entire prefix
|
@@ -796,8 +890,12 @@ class Binder < Base
|
|
796
890
|
|
797
891
|
prefix[pre].sort.each do |tag|
|
798
892
|
size, ents = bnd.read_tag(tr, tag, 0, 0)
|
799
|
-
rows << ListTag %
|
800
|
-
|
893
|
+
rows << ListTag % [
|
894
|
+
_link_tag(env, tag, _escape_html(tag)),
|
895
|
+
size,
|
896
|
+
_link_tag(env, tag, 'Tag'),
|
897
|
+
_link_dock(env, tag, 'Docket')
|
898
|
+
]
|
801
899
|
end
|
802
900
|
end
|
803
901
|
|
@@ -880,58 +978,149 @@ class Binder < Base
|
|
880
978
|
|
881
979
|
|
882
980
|
EditForm =
|
883
|
-
"<form class='edit' method='post' action='%s/%s'
|
884
|
-
|
885
|
-
|
886
|
-
|
981
|
+
"<form class='edit' method='post' action='%s/%s' \
|
982
|
+
enctype='multipart/form-data' accept-charset='utf-8'>
|
983
|
+
<fieldset>
|
984
|
+
<legend>Basic Info</legend>
|
985
|
+
<label for='title'>Title:</label>
|
986
|
+
<input class='title' name='title' type='text' value='%s'><br>
|
987
|
+
<label for='time'>Time:</label>
|
988
|
+
<input name='time' type='text' value='%s'><br>
|
989
|
+
<label for='body'>Body:</label>
|
990
|
+
<textarea class='body' name='body'>%s</textarea>
|
991
|
+
</fieldset>
|
992
|
+
<fieldset>
|
993
|
+
<legend>Attachments</legend>
|
994
|
+
<table>
|
995
|
+
<tr><th>Name</th><th>Upload/Replace</th><th></th></tr>
|
996
|
+
%s</table>
|
997
|
+
</fieldset>
|
998
|
+
<fieldset>
|
999
|
+
<legend>Tags</legend>
|
1000
|
+
<table>
|
1001
|
+
%s</table>
|
1002
|
+
</fieldset>
|
1003
|
+
<input name='entry' type='hidden' value='%d'>
|
1004
|
+
<input name='revision' type='hidden' value='%d'>
|
1005
|
+
<input name='attcnt' id='attcnt' type='hidden' value='%d'>
|
1006
|
+
<input name='tagcnt' id='tagcnt' type='hidden' value='%d'>
|
1007
|
+
<input type='submit' name='save' value='Save Changes'>
|
1008
|
+
</form>
|
1009
|
+
<script>
|
1010
|
+
var addAttach = (function(){
|
1011
|
+
var count = %d;
|
1012
|
+
return function(elmt){
|
1013
|
+
count = count + 1;
|
1014
|
+
var row = document.createElement('tr');
|
1015
|
+
|
1016
|
+
var col1 = document.createElement('td');
|
1017
|
+
var inpt = document.createElement('input');
|
1018
|
+
inpt.setAttribute('type', 'text');
|
1019
|
+
inpt.setAttribute('name', 'attname' + count);
|
1020
|
+
inpt.setAttribute('class', 'attname');
|
1021
|
+
col1.appendChild(inpt);
|
1022
|
+
inpt = document.createElement('input');
|
1023
|
+
inpt.setAttribute('type', 'hidden');
|
1024
|
+
inpt.setAttribute('name', 'attnumb' + count);
|
1025
|
+
inpt.setAttribute('value', '0');
|
1026
|
+
col1.appendChild(inpt);
|
1027
|
+
row.appendChild(col1);
|
1028
|
+
|
1029
|
+
var col2 = document.createElement('td');
|
1030
|
+
inpt = document.createElement('input');
|
1031
|
+
inpt.setAttribute('type', 'file');
|
1032
|
+
inpt.setAttribute('name', 'attfile' + count);
|
1033
|
+
col2.appendChild(inpt);
|
1034
|
+
row.appendChild(col2);
|
1035
|
+
|
1036
|
+
var col3 = document.createElement('td');
|
1037
|
+
col3.appendChild(elmt.cloneNode(true));
|
1038
|
+
row.appendChild(col3);
|
1039
|
+
|
1040
|
+
var pcol = elmt.parentNode;
|
1041
|
+
pcol.removeChild(elmt);
|
1042
|
+
var prow = pcol.parentNode;
|
1043
|
+
var ptab = prow.parentNode;
|
1044
|
+
ptab.appendChild(row);
|
1045
|
+
|
1046
|
+
var acnt = document.getElementById('attcnt');
|
1047
|
+
acnt.setAttribute('value', count + 1);
|
1048
|
+
}
|
1049
|
+
})();
|
1050
|
+
var addTag = (function(){
|
1051
|
+
var count = %d;
|
1052
|
+
return function(elmt){
|
1053
|
+
count = count + 1;
|
1054
|
+
var row = document.createElement('tr');
|
1055
|
+
|
1056
|
+
var col1 = document.createElement('td');
|
1057
|
+
var pcol = elmt.parentNode;
|
1058
|
+
var prev = pcol.previousElementSibling.lastChild;
|
1059
|
+
var ptyp = prev.nodeName;
|
1060
|
+
if( ptyp == 'SELECT' ){
|
1061
|
+
var valu = prev.options[prev.selectedIndex].getAttribute('value');
|
1062
|
+
} else {
|
1063
|
+
var valu = '';
|
1064
|
+
}
|
1065
|
+
var inpt = document.createElement('input');
|
1066
|
+
inpt.setAttribute('name', 'tag' + count);
|
1067
|
+
inpt.setAttribute('class', 'tag');
|
1068
|
+
inpt.setAttribute('type', 'text');
|
1069
|
+
inpt.setAttribute('value', valu);
|
1070
|
+
col1.appendChild(inpt);
|
1071
|
+
row.appendChild(col1);
|
887
1072
|
|
888
|
-
|
1073
|
+
var col2 = document.createElement('td');
|
1074
|
+
var addb = document.getElementById('add_tag');
|
1075
|
+
addb.parentNode.removeChild(addb);
|
1076
|
+
col2.appendChild(addb);
|
1077
|
+
row.appendChild(col2);
|
889
1078
|
|
890
|
-
|
1079
|
+
var prow = pcol.parentNode;
|
1080
|
+
var ptab = prow.parentNode;
|
1081
|
+
ptab.appendChild(row);
|
891
1082
|
|
892
|
-
|
893
|
-
|
1083
|
+
var tcnt = document.getElementById('tagcnt');
|
1084
|
+
tcnt.setAttribute('value', count + 1);
|
1085
|
+
}
|
1086
|
+
})();
|
1087
|
+
</script>"
|
894
1088
|
|
895
|
-
"<label for='time'>Time:</label>" +
|
896
|
-
"<input name='time' type='text' value='%s'><br>\n" +
|
897
1089
|
|
898
|
-
|
899
|
-
|
1090
|
+
EditAttach =
|
1091
|
+
"<tr><td><input name='attname%d' class='attname' type='text' value='%s'>\
|
1092
|
+
<input name='attnumb%d' type='hidden' value='%d'>\
|
1093
|
+
</td><td><input name='attfile%d' type='file'></td><td>%s</td></tr>\n"
|
900
1094
|
|
901
|
-
"</fieldset>\n" +
|
902
|
-
"<fieldset><legend>Attachments</legend>\n%s</fieldset>\n" +
|
903
|
-
"<fieldset><legend>Tags</legend>\n%s</fieldset>\n" +
|
904
1095
|
|
905
|
-
|
906
|
-
|
1096
|
+
EditAttachButton =
|
1097
|
+
"<button type='button' class='add_row' id='add_attach' \
|
1098
|
+
onclick='addAttach(this)'>+</button>"
|
907
1099
|
|
908
|
-
EditFilePre =
|
909
|
-
"<table class='edit_file'>\n" +
|
910
|
-
"<tr><th>Name</th><th>Upload/Replace</th></tr>\n"
|
911
1100
|
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
"<td><input name='attfile%d' type='file'></td></tr>\n"
|
1101
|
+
EditTag =
|
1102
|
+
"<tr><td><input name='tag%d' class='tag' type='text' value='%s'></td>\
|
1103
|
+
<td>%s</td></tr>\n"
|
916
1104
|
|
917
|
-
EditFileCnt =
|
918
|
-
"</table>\n<input name='attcnt' type='hidden' value='%d'>\n"
|
919
1105
|
|
920
|
-
|
921
|
-
|
1106
|
+
EditTagSel =
|
1107
|
+
"<tr>
|
1108
|
+
<td>%s: <select name='pre%d'>
|
1109
|
+
<option value='%s: ' selected></option>
|
1110
|
+
%s</select></td>
|
1111
|
+
<td><button type='button' class='add_row' onclick='addTag(this)'>+\
|
1112
|
+
</button></td>
|
1113
|
+
</tr>\n"
|
922
1114
|
|
923
|
-
|
924
|
-
|
1115
|
+
|
1116
|
+
EditTagButton =
|
1117
|
+
"<button type='button' class='add_row' id='add_tag' onclick='addTag(this)'>+\
|
1118
|
+
</button>"
|
925
1119
|
|
926
|
-
EditTagSel =
|
927
|
-
"%s: <select name='tag%d'>" +
|
928
|
-
"<option value='' selected></option>%s</select><br>\n"
|
929
1120
|
|
930
1121
|
EditTagOpt =
|
931
|
-
|
1122
|
+
"<option value='%s: %s'>%s</option>\n"
|
932
1123
|
|
933
|
-
EditTagCnt =
|
934
|
-
"<input name='tagcnt' type='hidden' value='%d'>\n"
|
935
1124
|
|
936
1125
|
#####################################
|
937
1126
|
# Get edit form
|
@@ -940,6 +1129,7 @@ class Binder < Base
|
|
940
1129
|
|
941
1130
|
tr = _trans(env)
|
942
1131
|
|
1132
|
+
# get entry
|
943
1133
|
if path.empty?
|
944
1134
|
enum = 0
|
945
1135
|
rnum = 0
|
@@ -954,6 +1144,7 @@ class Binder < Base
|
|
954
1144
|
rnum = ent.revision
|
955
1145
|
end
|
956
1146
|
|
1147
|
+
# get tag list and prefixes
|
957
1148
|
lst = env['sgfa.binder'].read_list(tr)
|
958
1149
|
prefix = {}
|
959
1150
|
lst.each do |tag|
|
@@ -968,51 +1159,49 @@ class Binder < Base
|
|
968
1159
|
end
|
969
1160
|
end
|
970
1161
|
|
1162
|
+
# attachments
|
1163
|
+
atts = ''
|
1164
|
+
acnt = 0
|
1165
|
+
ent.attachments.each do |anum, hnum, name|
|
1166
|
+
atts << EditAttach % [acnt, _escape_html(name), acnt, anum, acnt, '']
|
1167
|
+
acnt += 1
|
1168
|
+
end
|
1169
|
+
atts << EditAttach % [acnt, '', acnt, 0, acnt, EditAttachButton]
|
1170
|
+
|
1171
|
+
# tags
|
971
1172
|
tags = ''
|
972
1173
|
cnt = 0
|
973
|
-
|
974
|
-
|
975
|
-
cnt += 1
|
976
|
-
end
|
977
|
-
prefix.each do |pre, lst|
|
1174
|
+
prefix.keys.sort.each do |pre|
|
1175
|
+
lst = prefix[pre]
|
978
1176
|
px = _escape_html(pre)
|
979
1177
|
opts = ''
|
980
|
-
lst.each do |post|
|
1178
|
+
lst.sort.each do |post|
|
981
1179
|
ex = _escape_html(post)
|
982
1180
|
opts << EditTagOpt % [px, ex, ex]
|
983
1181
|
end
|
984
|
-
tags << EditTagSel % [px, cnt, opts]
|
985
|
-
cnt += 1
|
986
|
-
end
|
987
|
-
5.times do |tg|
|
988
|
-
tags << EditTagNew % cnt
|
989
|
-
cnt += 1
|
990
|
-
end
|
991
|
-
tags << EditTagCnt % cnt
|
992
|
-
|
993
|
-
atts = "Attachments go here\n"
|
994
|
-
atts = EditFilePre.dup
|
995
|
-
cnt = 0
|
996
|
-
ent.attachments.each do |anum, hnum, name|
|
997
|
-
atts << EditFileEach % [cnt, _escape_html(name), cnt, anum, cnt]
|
1182
|
+
tags << EditTagSel % [px, cnt, px, opts]
|
998
1183
|
cnt += 1
|
999
1184
|
end
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1185
|
+
tcnt = 0
|
1186
|
+
ent.tags.sort.each do |tag|
|
1187
|
+
tags << EditTag % [tcnt, _escape_html(tag), '']
|
1188
|
+
tcnt += 1
|
1003
1189
|
end
|
1004
|
-
|
1190
|
+
tags << EditTag % [tcnt, '', EditTagButton]
|
1005
1191
|
|
1192
|
+
# the page
|
1006
1193
|
html = EditForm % [
|
1007
|
-
env['SCRIPT_NAME'], env['sgfa.jacket.url'],
|
1194
|
+
env['SCRIPT_NAME'], env['sgfa.jacket.url'],
|
1008
1195
|
_escape_html(ent.title), ent.time.localtime.strftime('%F %T %z'),
|
1009
|
-
_escape_html(ent.body), atts, tags,
|
1196
|
+
_escape_html(ent.body), atts, tags, enum, rnum, acnt+1, tcnt+1,
|
1197
|
+
acnt, tcnt
|
1010
1198
|
]
|
1011
1199
|
|
1012
1200
|
env['sgfa.status'] = :ok
|
1013
1201
|
env['sgfa.html'] = html
|
1014
1202
|
end # def _get_edit
|
1015
|
-
|
1203
|
+
|
1204
|
+
|
1016
1205
|
JacketPost = [
|
1017
1206
|
'entry',
|
1018
1207
|
'revision',
|
@@ -1029,6 +1218,13 @@ class Binder < Base
|
|
1029
1218
|
rck = Rack::Request.new(env)
|
1030
1219
|
params = rck.POST
|
1031
1220
|
|
1221
|
+
# stupid kludge to fix bad standards inplementation.
|
1222
|
+
# apparently browsers don't set the charset correctly so rack defaults
|
1223
|
+
# to ASCII-8BIT, and then everything dies when there's actually UTF-8
|
1224
|
+
# present. Since modern browsers actually send UTF-8 when we ask for
|
1225
|
+
# it, just force it to UTF-8 and hope for the best.
|
1226
|
+
params.each{ |key, val| val.force_encoding('utf-8') if val.is_a?(String) }
|
1227
|
+
|
1032
1228
|
# validate fields present
|
1033
1229
|
JacketPost.each do |fn|
|
1034
1230
|
next if params[fn]
|
@@ -1088,9 +1284,15 @@ class Binder < Base
|
|
1088
1284
|
|
1089
1285
|
# old file
|
1090
1286
|
else
|
1091
|
-
ent.rename(anum, name) if name != ''
|
1092
1287
|
ent.replace(anum, ftmp) if ftmp
|
1093
|
-
|
1288
|
+
if name != ''
|
1289
|
+
ent.rename(anum, name)
|
1290
|
+
elsif ftmp
|
1291
|
+
ent.rename(anum, file[:filename])
|
1292
|
+
else
|
1293
|
+
ent.delete(anum)
|
1294
|
+
end
|
1295
|
+
end
|
1094
1296
|
|
1095
1297
|
end
|
1096
1298
|
|
@@ -1121,6 +1323,13 @@ class Binder < Base
|
|
1121
1323
|
rck = Rack::Request.new(env)
|
1122
1324
|
params = rck.POST
|
1123
1325
|
|
1326
|
+
# stupid kludge to fix bad standards inplementation.
|
1327
|
+
# apparently browsers don't set the charset correctly so rack defaults
|
1328
|
+
# to ASCII-8BIT, and then everything dies when there's actually UTF-8
|
1329
|
+
# present. Since modern browsers actually send UTF-8 when we ask for
|
1330
|
+
# it, just force it to UTF-8 and hope for the best.
|
1331
|
+
params.each{ |key, val| val.force_encoding('utf-8') if val.is_a?(String) }
|
1332
|
+
|
1124
1333
|
tr = _trans(env)
|
1125
1334
|
tr[:title] = 'Test title'
|
1126
1335
|
tr[:body] = 'Test description of action'
|