mhc 1.0.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/.gitignore +27 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/COPYRIGHT +28 -0
- data/Gemfile +8 -0
- data/README.org +209 -0
- data/Rakefile +13 -0
- data/bin/mhc +312 -0
- data/emacs/Cask +25 -0
- data/emacs/Makefile +58 -0
- data/emacs/mhc-calendar.el +1723 -0
- data/emacs/mhc-calfw.el +135 -0
- data/emacs/mhc-compat.el +90 -0
- data/emacs/mhc-date.el +642 -0
- data/emacs/mhc-day.el +149 -0
- data/emacs/mhc-db.el +158 -0
- data/emacs/mhc-draft.el +211 -0
- data/emacs/mhc-e21.el +167 -0
- data/emacs/mhc-face.el +236 -0
- data/emacs/mhc-file.el +224 -0
- data/emacs/mhc-guess.el +648 -0
- data/emacs/mhc-header.el +176 -0
- data/emacs/mhc-logic.el +563 -0
- data/emacs/mhc-message.el +130 -0
- data/emacs/mhc-minibuf.el +466 -0
- data/emacs/mhc-misc.el +248 -0
- data/emacs/mhc-mua.el +260 -0
- data/emacs/mhc-parse.el +286 -0
- data/emacs/mhc-process.el +35 -0
- data/emacs/mhc-ps.el +1174 -0
- data/emacs/mhc-record.el +201 -0
- data/emacs/mhc-schedule.el +202 -0
- data/emacs/mhc-summary.el +763 -0
- data/emacs/mhc-sync.el +158 -0
- data/emacs/mhc-vars.el +149 -0
- data/emacs/mhc.el +1114 -0
- data/icons/Anniversary.xbm +6 -0
- data/icons/Anniversary.xpm +27 -0
- data/icons/Birthday.xbm +6 -0
- data/icons/Birthday.xpm +25 -0
- data/icons/Business.xbm +6 -0
- data/icons/Business.xpm +24 -0
- data/icons/CheckBox.xbm +6 -0
- data/icons/CheckBox.xpm +24 -0
- data/icons/CheckedBox.xbm +6 -0
- data/icons/CheckedBox.xpm +25 -0
- data/icons/Conflict.xbm +6 -0
- data/icons/Conflict.xpm +22 -0
- data/icons/Date.xbm +6 -0
- data/icons/Date.xpm +29 -0
- data/icons/Holiday.xbm +6 -0
- data/icons/Holiday.xpm +25 -0
- data/icons/Link.xbm +6 -0
- data/icons/Link.xpm +25 -0
- data/icons/Other.xbm +6 -0
- data/icons/Other.xpm +28 -0
- data/icons/Party.xbm +6 -0
- data/icons/Party.xpm +23 -0
- data/icons/Private.xbm +6 -0
- data/icons/Private.xpm +26 -0
- data/icons/Recurrence.xbm +6 -0
- data/icons/Recurrence.xpm +98 -0
- data/icons/Vacation.xbm +6 -0
- data/icons/Vacation.xpm +26 -0
- data/lib/mhc.rb +45 -0
- data/lib/mhc/builder.rb +64 -0
- data/lib/mhc/caldav.rb +304 -0
- data/lib/mhc/calendar.rb +106 -0
- data/lib/mhc/command.rb +13 -0
- data/lib/mhc/command/cache.rb +14 -0
- data/lib/mhc/command/completions.rb +108 -0
- data/lib/mhc/command/init.rb +133 -0
- data/lib/mhc/command/scan.rb +33 -0
- data/lib/mhc/command/sync.rb +22 -0
- data/lib/mhc/config.rb +229 -0
- data/lib/mhc/converter.rb +330 -0
- data/lib/mhc/datastore.rb +164 -0
- data/lib/mhc/date_enumerator.rb +274 -0
- data/lib/mhc/date_frame.rb +124 -0
- data/lib/mhc/date_helper.rb +49 -0
- data/lib/mhc/etag.rb +68 -0
- data/lib/mhc/event.rb +396 -0
- data/lib/mhc/formatter.rb +312 -0
- data/lib/mhc/logger.rb +94 -0
- data/lib/mhc/modifier.rb +149 -0
- data/lib/mhc/occurrence.rb +94 -0
- data/lib/mhc/occurrence_enumerator.rb +113 -0
- data/lib/mhc/property_value.rb +33 -0
- data/lib/mhc/property_value/date.rb +190 -0
- data/lib/mhc/property_value/integer.rb +15 -0
- data/lib/mhc/property_value/list.rb +41 -0
- data/lib/mhc/property_value/period.rb +49 -0
- data/lib/mhc/property_value/range.rb +100 -0
- data/lib/mhc/property_value/recurrence_condition.rb +272 -0
- data/lib/mhc/property_value/text.rb +11 -0
- data/lib/mhc/property_value/time.rb +45 -0
- data/lib/mhc/query.rb +210 -0
- data/lib/mhc/sync.rb +46 -0
- data/lib/mhc/sync/driver.rb +108 -0
- data/lib/mhc/sync/status.rb +70 -0
- data/lib/mhc/sync/status_manager.rb +142 -0
- data/lib/mhc/sync/strategy.rb +233 -0
- data/lib/mhc/sync/syncinfo.rb +98 -0
- data/lib/mhc/templates/config.yml.erb +142 -0
- data/lib/mhc/version.rb +4 -0
- data/lib/mhc/webdav.rb +319 -0
- data/mhc.gemspec +24 -0
- data/samples/DOT.mhc-config.yml +116 -0
- data/samples/japanese-holidays.mhcc +153 -0
- data/samples/mhc-completions.zsh +11 -0
- data/spec/mhc_spec.rb +682 -0
- data/spec/spec_helper.rb +9 -0
- data/xpm/close.xpm +18 -0
- data/xpm/delete.xpm +19 -0
- data/xpm/exit.xpm +18 -0
- data/xpm/month.xpm +18 -0
- data/xpm/next.xpm +18 -0
- data/xpm/next2.xpm +18 -0
- data/xpm/next_year.xpm +18 -0
- data/xpm/open.xpm +19 -0
- data/xpm/prev.xpm +18 -0
- data/xpm/prev2.xpm +18 -0
- data/xpm/prev_year.xpm +18 -0
- data/xpm/save.xpm +19 -0
- data/xpm/today.xpm +18 -0
- metadata +214 -0
@@ -0,0 +1,330 @@
|
|
1
|
+
module Mhc
|
2
|
+
class Converter
|
3
|
+
class Emacs
|
4
|
+
# return cfw:event structure
|
5
|
+
#
|
6
|
+
# (defstruct cfw:event
|
7
|
+
# title ; event title [string]
|
8
|
+
# start-date ; start date of the event [cfw:date]
|
9
|
+
# start-time ; start time of the event (optional)
|
10
|
+
# end-date ; end date of the event [cfw:date] (optional)
|
11
|
+
# end-time ; end of the event (optional)
|
12
|
+
# description ; event description [string] (optional)
|
13
|
+
# location ; location [strting] (optional)
|
14
|
+
# source ; [internal] source of the event
|
15
|
+
# )
|
16
|
+
def to_calfw(ev)
|
17
|
+
hash = {
|
18
|
+
:title => ev.subject.to_s,
|
19
|
+
:start_date => "",
|
20
|
+
:start_time => "",
|
21
|
+
:end_date => "",
|
22
|
+
:end_time => "",
|
23
|
+
:description => "",
|
24
|
+
:location => "",
|
25
|
+
:source => ""
|
26
|
+
}
|
27
|
+
to_emacs_plist(hash)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_emacs(obj)
|
31
|
+
case obj
|
32
|
+
when Array
|
33
|
+
to_emacs_list(obj)
|
34
|
+
when Hash
|
35
|
+
to_emacs_plist(obj)
|
36
|
+
else
|
37
|
+
to_emacs_string(obj)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_emacs_symbol(obj)
|
42
|
+
":" + obj.to_s.downcase.gsub('_', '-')
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_emacs_string(str)
|
46
|
+
# 1. quote " and \
|
47
|
+
# 2. surround by "
|
48
|
+
'"' + str.to_s.toutf8.gsub(/[\"\\]/, '\\\\\&') + '"'
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_emacs_plist(hash)
|
52
|
+
wrap(hash.map{|key,val| "#{to_emacs_symbol(key)} #{to_emacs(val)}"}.join(" "))
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_emacs_list(array)
|
56
|
+
wrap(array.map{|val| to_emacs(val)}.join(" "))
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def wrap(obj)
|
61
|
+
"(" + obj.to_s + ")"
|
62
|
+
end
|
63
|
+
end # class Emacs
|
64
|
+
|
65
|
+
class Icalendar
|
66
|
+
|
67
|
+
def to_ics(event)
|
68
|
+
return to_icalendar(event).to_s
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_ics_string(event)
|
72
|
+
ical = RiCal.Calendar
|
73
|
+
ical.prodid = Mhc::PRODID
|
74
|
+
ical.events << to_icalendar(event)
|
75
|
+
return ical.to_s
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_icalendar(event)
|
79
|
+
icalendar = RiCal.Event do |iev|
|
80
|
+
iev.rrule = event.recurrence_condition.to_ics(dtstart(event), event.duration.last) if event.recurring?
|
81
|
+
iev.exdates = [exdates(event)] if exdates(event)
|
82
|
+
iev.rdates = [rdates(event)] if rdates(event)
|
83
|
+
iev.created = created(event).utc.strftime("%Y%m%dT%H%M%SZ")
|
84
|
+
iev.categories = event.categories.to_a unless event.categories.empty?
|
85
|
+
iev.location = event.location.to_s unless event.location.to_s.empty?
|
86
|
+
iev.last_modified = last_modified(event).utc.strftime("%Y%m%dT%H%M%SZ")
|
87
|
+
iev.uid = event.uid.to_s
|
88
|
+
iev.dtstart = dtstart(event)
|
89
|
+
iev.dtend = dtend(event)
|
90
|
+
iev.summary = event.subject.to_s
|
91
|
+
iev.description = event.description.to_s
|
92
|
+
iev.sequence = (event.sequence.to_i || 0)
|
93
|
+
iev.dtstamp = ::Time.now.utc.strftime("%Y%m%dT%H%M%SZ")
|
94
|
+
iev.add_x_property("X-SC-Recurrence-Tag", event.recurrence_tag.to_s) if event.recurrence_tag.to_s != ""
|
95
|
+
iev.add_x_property("X-SC-Mission-Tag", event.mission_tag.to_s) if event.mission_tag.to_s != ""
|
96
|
+
end
|
97
|
+
return icalendar
|
98
|
+
end
|
99
|
+
|
100
|
+
################################################################
|
101
|
+
private
|
102
|
+
|
103
|
+
# DTSTART (RFC5445:iCalendar) has these two meanings:
|
104
|
+
# 1) first ocurrence date of recurrence events
|
105
|
+
# 2) start date of a single-shot event
|
106
|
+
#
|
107
|
+
# In MHC, DTSTART should be calculated as:
|
108
|
+
#
|
109
|
+
# if a MHC article has a Cond: field,
|
110
|
+
# + DTSTART is calculated from Duration: and Cond: field.
|
111
|
+
# + Additional Day: field is recognized as RDATE.
|
112
|
+
# else
|
113
|
+
# + DTSTART is calculated from a first entry of Days: field.
|
114
|
+
# + Remains in Day: field is recognized as RDATE.
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
def dtstart(event)
|
118
|
+
if event.recurring?
|
119
|
+
Mhc::OccurrenceEnumerator.new(event, empty_dates, empty_dates, event.recurrence_condition, event.duration).first.dtstart
|
120
|
+
else
|
121
|
+
Mhc::OccurrenceEnumerator.new(event, event.dates, empty_dates, empty_condition, empty_duration).first.dtstart
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def dtend(event)
|
126
|
+
if event.recurring?
|
127
|
+
Mhc::OccurrenceEnumerator.new(event, empty_dates, empty_dates, event.recurrence_condition, event.duration).first.dtend
|
128
|
+
else
|
129
|
+
Mhc::OccurrenceEnumerator.new(event, event.dates, empty_dates, empty_condition, empty_duration).first.dtend
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def rdates(event)
|
134
|
+
return nil if event.dates.empty?
|
135
|
+
ocs = Mhc::OccurrenceEnumerator.new(event, event.dates, empty_dates, empty_condition, empty_duration).map {|oc| oc.dtstart}
|
136
|
+
if event.recurring?
|
137
|
+
ocs
|
138
|
+
else
|
139
|
+
ocs = ocs[1..-1]
|
140
|
+
return nil if ocs.empty?
|
141
|
+
return ocs
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def exdates(event)
|
146
|
+
return nil if event.exceptions.empty?
|
147
|
+
ocs = Mhc::OccurrenceEnumerator.new(event, event.exceptions, empty_dates, empty_condition, empty_duration).map {|oc| oc.dtstart }
|
148
|
+
return ocs
|
149
|
+
end
|
150
|
+
|
151
|
+
def empty_duration
|
152
|
+
Mhc::PropertyValue::Range.new(Mhc::PropertyValue::Date)
|
153
|
+
end
|
154
|
+
|
155
|
+
def empty_dates
|
156
|
+
Mhc::PropertyValue::List.new(Mhc::PropertyValue::Range.new(Mhc::PropertyValue::Date.new))
|
157
|
+
end
|
158
|
+
|
159
|
+
def empty_condition
|
160
|
+
Mhc::PropertyValue::RecurrenceCondition.new
|
161
|
+
end
|
162
|
+
|
163
|
+
def created(event)
|
164
|
+
if event.path
|
165
|
+
File.ctime(event.path)
|
166
|
+
else
|
167
|
+
::Time.utc(2014, 1, 1)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def last_modified(event)
|
172
|
+
if event.path
|
173
|
+
File.mtime(event.path)
|
174
|
+
else
|
175
|
+
::Time.utc(2014, 1, 1)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end # class Icalendar
|
179
|
+
|
180
|
+
class IcalendarImporter
|
181
|
+
def self.parse_ics(ics)
|
182
|
+
# * 3.8.1. Descriptive Component Properties
|
183
|
+
# ** CATEGORIES 3.8.1.2. Categories
|
184
|
+
# ** DESCRIPTION 3.8.1.5. Description
|
185
|
+
# ** LOCATION 3.8.1.7. Location
|
186
|
+
# ** SUMMARY 3.8.1.12. Summary
|
187
|
+
# * 3.8.2. Date and Time Component Properties
|
188
|
+
# ** DTEND 3.8.2.2. Date-Time End
|
189
|
+
# ** DTSTART 3.8.2.4. Date-Time Start
|
190
|
+
# ** DURATION 3.8.2.5. Duration
|
191
|
+
# * 3.8.4. Relationship Component Properties
|
192
|
+
# ** RECURRENCE-ID 3.8.4.4. Recurrence ID
|
193
|
+
# * 3.8.5. Recurrence Component Properties
|
194
|
+
# ** EXDATE 3.8.5.1. Exception Date-Times
|
195
|
+
# ** RDATE 3.8.5.2. Recurrence Date-Times
|
196
|
+
# ** RRULE 3.8.5.3. Recurrence Rule
|
197
|
+
# * 3.8.7. Change Management Component Properties
|
198
|
+
# ** SEQUENCE 3.8.7.4. Sequence Number
|
199
|
+
# * 3.8.8. Miscellaneous Component Properties
|
200
|
+
# ** X-FIELD 3.8.8.2. Non-Standard Properties
|
201
|
+
|
202
|
+
# DTSTART:
|
203
|
+
# Date part => X-SC-Duration: .first
|
204
|
+
# Time part => X-SC-Time: .first
|
205
|
+
# DTEND
|
206
|
+
# Date part =>
|
207
|
+
# DTEND - DTSTART = 1day
|
208
|
+
# DTEND - DTSTART > 1days
|
209
|
+
# Day:
|
210
|
+
# RRULE:
|
211
|
+
# X-SC-Cond:
|
212
|
+
# UNTIL:
|
213
|
+
# X-SC-Duration: .last
|
214
|
+
# RDATES:
|
215
|
+
# X-SC-Day:
|
216
|
+
# EXDATES:
|
217
|
+
# X-SC-Day: !YYYYMMDD
|
218
|
+
#
|
219
|
+
ical = RiCal.parse_string(ics).first
|
220
|
+
return nil unless ical
|
221
|
+
|
222
|
+
iev = ical.events.first
|
223
|
+
allday = !iev.dtstart.respond_to?(:hour)
|
224
|
+
recurring = !iev.rrule.empty?
|
225
|
+
|
226
|
+
# X-SC-Day: (from DTSTART, DTEND)
|
227
|
+
# for recurring event, DTSTSRT is a start part of X-SC-Duration:
|
228
|
+
dates = []
|
229
|
+
unless recurring
|
230
|
+
date = tz_convert(iev.dtstart).strftime("%Y%m%d")
|
231
|
+
if allday && (iev.dtend - iev.dtstart).to_i > 1
|
232
|
+
date += "-" + (iev.dtend - 1).to_time.strftime("%Y%m%d")
|
233
|
+
end
|
234
|
+
dates << date
|
235
|
+
end
|
236
|
+
|
237
|
+
# X-SC-Day: (from RDATE, EXDATE)
|
238
|
+
dates += iev.rdate.flatten.map{|d| d.to_time.strftime("%Y%m%d")}
|
239
|
+
exdates = iev.exdate.flatten.map{|d| d.to_time.strftime("!%Y%m%d")}
|
240
|
+
|
241
|
+
# X-SC-Time:
|
242
|
+
unless allday
|
243
|
+
time = tz_convert(iev.dtstart).strftime("%H:%M")
|
244
|
+
if iev.dtend
|
245
|
+
time += "-" + tz_convert(iev.dtend).strftime("%H:%M")
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
ev = Mhc::Event.parse "X-SC-Subject: #{iev.summary}\n" +
|
250
|
+
"X-SC-Location: #{iev.location}\n" +
|
251
|
+
"X-SC-Day: #{(dates + exdates).join(' ')}\n" +
|
252
|
+
"X-SC-Time: #{time}\n" +
|
253
|
+
"X-SC-Category: #{iev.categories.to_a.join(' ')}\n" +
|
254
|
+
"X-SC-Mission-Tag: #{iev.x_sc_mission_tag.first}\n" +
|
255
|
+
"X-SC-Recurrence-Tag: #{iev.x_sc_recurrence_tag.first}\n" +
|
256
|
+
"X-SC-Cond: \n" +
|
257
|
+
"X-SC-Duration: \n" +
|
258
|
+
"X-SC-Alarm: \n" +
|
259
|
+
"X-SC-Record-Id: #{iev.uid}\n" +
|
260
|
+
"X-SC-Sequence: #{iev.sequence.to_i}\n\n" + iev.description.to_s +
|
261
|
+
if $MHC_DEBUG_FOR_DEVELOPER # FIXME: should introduce good logger and debug scheme
|
262
|
+
ical.to_s.force_encoding("ASCII-8BIT").gsub(/\r\n/, "\n")
|
263
|
+
else
|
264
|
+
""
|
265
|
+
end
|
266
|
+
|
267
|
+
# X-SC-Cond:
|
268
|
+
ev.recurrence_condition.set_from_ics(iev.rrule.first, tz_convert(iev.dtstart))
|
269
|
+
|
270
|
+
# X-SC-Duration: is only for recurring articles
|
271
|
+
if recurring
|
272
|
+
duration_string = tz_convert(iev.dtstart).strftime("%Y%m%d") + "-"
|
273
|
+
if iev.rrule.first.to_s.match(/until=([^;]+)/i)
|
274
|
+
duration_string += parse_ical_datetime($1).strftime("%Y%m%d")
|
275
|
+
end
|
276
|
+
ev.duration = duration_string
|
277
|
+
end
|
278
|
+
|
279
|
+
return ev
|
280
|
+
end
|
281
|
+
|
282
|
+
private
|
283
|
+
|
284
|
+
def self.tz_convert(datetime, src_tzid: nil, dst_tzid: nil)
|
285
|
+
return datetime unless datetime.respond_to?(:hour)
|
286
|
+
|
287
|
+
dst_tzid ||= Mhc.default_tzid
|
288
|
+
src_tzid ||= if datetime.respond_to?(:tzid) and datetime.tzid
|
289
|
+
datetime.tzid
|
290
|
+
else
|
291
|
+
Mhc.default_tzid
|
292
|
+
end
|
293
|
+
dst_tz = TZInfo::Timezone.get(dst_tzid)
|
294
|
+
src_tz = TZInfo::Timezone.get(src_tzid)
|
295
|
+
|
296
|
+
utc = Time.utc(datetime.year, datetime.month, datetime.day,
|
297
|
+
datetime.hour, datetime.min, datetime.sec)
|
298
|
+
|
299
|
+
time1 = src_tz.local_to_utc(utc)
|
300
|
+
time1.tzid = src_tzid if time1.respond_to?(:tzid)
|
301
|
+
|
302
|
+
time = dst_tz.utc_to_local(time1)
|
303
|
+
time.tzid = dst_tzid if time.respond_to?(:tzid)
|
304
|
+
|
305
|
+
return time
|
306
|
+
end
|
307
|
+
|
308
|
+
def self.parse_ical_datetime(datetime_string, dst_tzid = nil)
|
309
|
+
src_tzid = case datetime_string
|
310
|
+
when /TZID=([^;]+)/
|
311
|
+
$1
|
312
|
+
when /\d{8}T\d{6}Z/
|
313
|
+
"UTC"
|
314
|
+
else
|
315
|
+
Mhc.default_tzid
|
316
|
+
end
|
317
|
+
|
318
|
+
dst_tzid ||= Mhc.default_tzid
|
319
|
+
|
320
|
+
if /^(\d{4})(\d\d)(\d\d)(?:T(\d\d)(\d\d)(\d\d)Z?)?$/ =~ datetime_string
|
321
|
+
time = Time.utc($1, $2, $3, $4, $5, $6)
|
322
|
+
return tz_convert(time, src_tzid: src_tzid, dst_tzid: dst_tzid)
|
323
|
+
else
|
324
|
+
raise ArgumentError
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
end # IcalendarImporter
|
329
|
+
end # Converter
|
330
|
+
end # Mhc
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
module Mhc
|
4
|
+
class DataStore
|
5
|
+
def initialize(basedir)
|
6
|
+
unless basedir and File.directory?(File.expand_path(basedir.to_s))
|
7
|
+
raise Mhc::ConfigurationError, "datastore directory '#{basedir}' not found"
|
8
|
+
end
|
9
|
+
@basedir = Pathname.new(File.expand_path(basedir))
|
10
|
+
@cache = Cache.new(File.expand_path("status/cache/events.pstore", @basedir))
|
11
|
+
end
|
12
|
+
|
13
|
+
def entries(date_range = nil)
|
14
|
+
if date_range
|
15
|
+
int_range = date_range.min.absolute_from_epoch .. date_range.max.absolute_from_epoch
|
16
|
+
end
|
17
|
+
|
18
|
+
Enumerator.new do |yielder|
|
19
|
+
["inbox", "spool", "presets"].each do |slot|
|
20
|
+
dir = File.expand_path(slot, @basedir)
|
21
|
+
next unless File.directory?(dir)
|
22
|
+
|
23
|
+
Dir.chdir(dir) do
|
24
|
+
Dir.foreach(".") do |ent|
|
25
|
+
parse_mhcc(ent).each {|ev| yielder << ev} if /\.mhcc$/ =~ ent
|
26
|
+
next unless /\.mhc$/ =~ ent
|
27
|
+
uid = $`
|
28
|
+
cache_entry = @cache.lookup(uid, ent)
|
29
|
+
if !date_range || cache_entry.involved?(int_range)
|
30
|
+
yielder << Event.parse_file(File.expand_path(ent))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@cache.save
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def logger
|
40
|
+
@logger ||= Mhc::Logger.new(@logfile)
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_by_uid(uid)
|
44
|
+
path = find_path(uid)
|
45
|
+
return nil unless path
|
46
|
+
return Event.parse_file(path)
|
47
|
+
end
|
48
|
+
|
49
|
+
def create(event)
|
50
|
+
if find_by_uid(event.uid)
|
51
|
+
raise "Already exist uid:#{uid} in #{@basedir}"
|
52
|
+
end
|
53
|
+
File.open(path, "w") do |f|
|
54
|
+
f.write(event.dump)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def update(event)
|
59
|
+
unless path = uid_to_path(event.uid)
|
60
|
+
raise "Not found uid:#{uid} in #{@basedir}"
|
61
|
+
end
|
62
|
+
File.open(path, "w") do |f|
|
63
|
+
f.write(event.dump)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def delete(uid_or_event)
|
68
|
+
uid = if uid_or_event.respond_to?(:uid)
|
69
|
+
uid_or_event.uid
|
70
|
+
else
|
71
|
+
uid_or_event
|
72
|
+
end
|
73
|
+
if path = find_path(uid)
|
74
|
+
File.delete(path)
|
75
|
+
else
|
76
|
+
raise "Not found uid:#{uid} in #{@basedir}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
################################################################
|
81
|
+
private
|
82
|
+
|
83
|
+
def parse_mhcc(filename)
|
84
|
+
string = File.open(filename).read.scrub.gsub(/^\s*#.*$/, "").strip
|
85
|
+
string.split(/\n\n\n*/).map do |header|
|
86
|
+
Event.parse(header)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def find_path(uid)
|
91
|
+
glob = @basedir + ('**/' + uid + '.mhc')
|
92
|
+
return Dir.glob(glob).first
|
93
|
+
end
|
94
|
+
|
95
|
+
def uid_to_path(uid)
|
96
|
+
return @basedir + ('spool/' + uid + '.mhc')
|
97
|
+
end
|
98
|
+
|
99
|
+
end # class DataStore
|
100
|
+
end # module Mhc
|
101
|
+
|
102
|
+
module Mhc
|
103
|
+
class DataStore
|
104
|
+
class Cache
|
105
|
+
require 'pstore'
|
106
|
+
|
107
|
+
def initialize(cache_filename)
|
108
|
+
@pstore = PStore.new(cache_filename)
|
109
|
+
load
|
110
|
+
end
|
111
|
+
|
112
|
+
def lookup(uid, filename)
|
113
|
+
unless c = get(uid) and File.mtime(filename).to_i <= c.mtime
|
114
|
+
c = CacheEntry.new(filename)
|
115
|
+
put(uid, c)
|
116
|
+
end
|
117
|
+
return c
|
118
|
+
end
|
119
|
+
|
120
|
+
def save
|
121
|
+
return self unless @dirty
|
122
|
+
@pstore.transaction do
|
123
|
+
@pstore["root"] = @db
|
124
|
+
end
|
125
|
+
@dirty = false
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def get(uid)
|
131
|
+
@db[uid]
|
132
|
+
end
|
133
|
+
|
134
|
+
def put(uid, value)
|
135
|
+
@db[uid] = value
|
136
|
+
@dirty = true
|
137
|
+
end
|
138
|
+
|
139
|
+
def load
|
140
|
+
@pstore.transaction do
|
141
|
+
@db = @pstore["root"] || {}
|
142
|
+
end
|
143
|
+
@dirty = false
|
144
|
+
end
|
145
|
+
|
146
|
+
end # class Cache
|
147
|
+
|
148
|
+
class CacheEntry
|
149
|
+
attr_reader :mtime, :range
|
150
|
+
|
151
|
+
def initialize(filename)
|
152
|
+
@mtime, event = File.mtime(filename).to_i, Event.parse_file(filename)
|
153
|
+
@range = event.range.min.absolute_from_epoch ..
|
154
|
+
event.range.max.absolute_from_epoch
|
155
|
+
end
|
156
|
+
|
157
|
+
def involved?(range)
|
158
|
+
range.min <= @range.max && @range.min <= range.max
|
159
|
+
end
|
160
|
+
|
161
|
+
end # class CacheEntry
|
162
|
+
|
163
|
+
end # class DataStore
|
164
|
+
end # module Mhc
|