mingle_events 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/Gemfile +2 -4
  2. data/lib/mingle_events.rb +19 -3
  3. data/lib/mingle_events/entry_cache.rb +129 -0
  4. data/lib/mingle_events/feed/author.rb +12 -23
  5. data/lib/mingle_events/feed/category.rb +2 -0
  6. data/lib/mingle_events/feed/changes.rb +19 -50
  7. data/lib/mingle_events/feed/entry.rb +16 -21
  8. data/lib/mingle_events/feed/links.rb +3 -3
  9. data/lib/mingle_events/feed/page.rb +5 -5
  10. data/lib/mingle_events/http.rb +50 -0
  11. data/lib/mingle_events/mingle_basic_auth_access.rb +17 -50
  12. data/lib/mingle_events/mingle_hmac_auth_access.rb +22 -0
  13. data/lib/mingle_events/mingle_oauth_access.rb +10 -30
  14. data/lib/mingle_events/poller.rb +4 -3
  15. data/lib/mingle_events/processors/author_filter.rb +7 -7
  16. data/lib/mingle_events/processors/card_data.rb +15 -15
  17. data/lib/mingle_events/project_custom_properties.rb +15 -15
  18. data/lib/mingle_events/project_event_fetcher.rb +21 -110
  19. data/lib/mingle_events/xml.rb +101 -0
  20. data/lib/mingle_events/zip_directory.rb +98 -0
  21. data/test/mingle_events/entry_cache_test.rb +75 -0
  22. data/test/mingle_events/feed/author_test.rb +2 -2
  23. data/test/mingle_events/feed/changes_test.rb +22 -22
  24. data/test/mingle_events/feed/entry_test.rb +12 -12
  25. data/test/mingle_events/feed/links_test.rb +5 -5
  26. data/test/mingle_events/mingle_basic_auth_access_test.rb +15 -0
  27. data/test/mingle_events/mingle_hmac_auth_access_test.rb +15 -0
  28. data/test/mingle_events/mingle_oauth_access_test.rb +15 -0
  29. data/test/mingle_events/poller_test.rb +1 -1
  30. data/test/mingle_events/processors/author_filter_test.rb +5 -5
  31. data/test/mingle_events/processors/card_data_test.rb +12 -17
  32. data/test/mingle_events/project_custom_properties_test.rb +2 -3
  33. data/test/mingle_events/project_event_fetcher_test.rb +64 -8
  34. data/test/mingle_events/xml_test.rb +80 -0
  35. data/test/mingle_events/zip_directory_test.rb +44 -0
  36. data/test/test_helper.rb +36 -27
  37. metadata +121 -62
@@ -14,27 +14,27 @@ module MingleEvents
14
14
  end
15
15
 
16
16
  def entries
17
- @entries ||= page_as_document.search('entry').map do |entry_element|
17
+ @entries ||= page_as_document.select_all('./atom:feed/atom:entry').map do |entry_element|
18
18
  Entry.new(entry_element)
19
19
  end
20
20
  end
21
21
 
22
22
  def next
23
- next_url_element = page_as_document.at("link[@rel='next']")
23
+ next_url_element = page_as_document.select("./atom:feed/atom:link[@rel='next']")
24
24
  if next_url_element.nil?
25
25
  nil
26
26
  else
27
- Page.new(next_url_element["href"], @mingle_access)
27
+ Page.new(next_url_element.attr("href"), @mingle_access)
28
28
  end
29
29
  end
30
30
 
31
31
  private
32
32
 
33
33
  def page_as_document
34
- @page_as_document ||= Nokogiri::XML(@mingle_access.fetch_page(@url)).remove_namespaces!
34
+ @page_as_document ||= Xml.parse(@mingle_access.fetch_page(@url), ATOM_AND_MINGLE_NS)
35
35
  end
36
36
 
37
37
  end
38
38
 
39
39
  end
40
- end
40
+ end
@@ -0,0 +1,50 @@
1
+ module MingleEvents
2
+ module Http
3
+ extend self
4
+
5
+ MAX_RETRY_TIMES = 5
6
+
7
+ # get response body for a url, a block can be passed in for request pre-processing
8
+ def get(url, retry_count=0, &block)
9
+ rsp = fetch_page_response(url, &block)
10
+ case rsp
11
+ when Net::HTTPSuccess
12
+ rsp.body
13
+ when Net::HTTPUnauthorized
14
+ raise HttpError.new(rsp, url, %{
15
+ If you think you are passing correct credentials, please check
16
+ that you have enabled Mingle for basic authentication.
17
+ See <http://www.thoughtworks-studios.com/mingle/3.3/help/configuring_mingle_authentication.html>.})
18
+ when Net::HTTPBadGateway, Net::HTTPServiceUnavailable, Net::HTTPGatewayTimeOut
19
+ raise HttpError.new(rsp, url) if retry_count >= MAX_RETRY_TIMES
20
+ cooldown = retry_count * 2
21
+ MingleEvents.log.info "Getting service error when get page at #{url}, retry after #{cooldown}s..."
22
+ sleep cooldown
23
+ get(url, retry_count + 1, &block)
24
+ else
25
+ raise HttpError.new(rsp, url)
26
+ end
27
+ end
28
+
29
+ private
30
+ def fetch_page_response(url, &block)
31
+ uri = URI.parse(url)
32
+ http = Net::HTTP.new(uri.host, uri.port)
33
+ path = uri.request_uri
34
+
35
+ if uri.scheme == 'https'
36
+ http.use_ssl = true
37
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
38
+ end
39
+
40
+ MingleEvents.log.info "Fetching page at #{path}..."
41
+
42
+ start = Time.now
43
+ req = Net::HTTP::Get.new(path)
44
+ yield(req) if block_given?
45
+ rsp = http.request(req)
46
+ MingleEvents.log.info "...#{path} fetched in #{Time.now - start} seconds."
47
+ rsp
48
+ end
49
+ end
50
+ end
@@ -1,70 +1,37 @@
1
- module MingleEvents
2
-
1
+ module MingleEvents
2
+
3
3
  # Supports fetching of Mingle resources using HTTP basic auth.
4
4
  # Please only use this class to access resources over HTTPS so
5
5
  # as not to send credentials over plain-text connections.
6
6
  class MingleBasicAuthAccess
7
-
8
- attr_reader :base_url
9
-
10
- BASIC_AUTH_HTTP_WARNING = %{
7
+ BASIC_AUTH_HTTP_WARNING = %{
11
8
  WARNING!!!
12
- It looks like you are using basic authentication over a plain-text HTTP connection.
9
+ It looks like you are using basic authentication over a plain-text HTTP connection.
13
10
  We HIGHLY recommend AGAINST this practice. You should only use basic authentication over
14
11
  a secure HTTPS connection. Instructions for enabling HTTPS/SSL in Mingle can be found at
15
12
  <http://www.thoughtworks-studios.com/mingle/3.3/help/advanced_mingle_configuration.html>
16
13
  WARNING!!
17
14
  }
15
+ attr_reader :base_url
18
16
 
19
- def initialize(base_url, username, password)
17
+ def initialize(base_url, username, password, http=Http)
20
18
  @base_url = base_url
21
19
  @username = username
22
20
  @password = password
21
+ @http = http
23
22
  end
24
-
25
- # Fetch the content at location via HTTP. Throws error if non-200 response.
26
- def fetch_page(location)
27
- rsp = fetch_page_response(location)
28
- case rsp
29
- when Net::HTTPSuccess
30
- rsp.body
31
- when Net::HTTPUnauthorized
32
- raise HttpError.new(rsp, location, %{
33
- If you think you are passing correct credentials, please check
34
- that you have enabled Mingle for basic authentication.
35
- See <http://www.thoughtworks-studios.com/mingle/3.3/help/configuring_mingle_authentication.html>.})
36
- else
37
- raise HttpError.new(rsp, location)
23
+
24
+ def fetch_page(location)
25
+ location = @base_url + location if location[0..0] == '/'
26
+ @http.get(location) do |req|
27
+ MingleEvents.log.warn(BASIC_AUTH_HTTP_WARNING) if URI.parse(location).scheme == 'http'
28
+ req['authorization'] = basic_encode(@username, @password)
38
29
  end
39
30
  end
40
-
41
- private
42
-
43
- def fetch_page_response(location)
44
- location = @base_url + location if location[0..0] == '/'
45
-
46
- uri = URI.parse(location)
47
- http = Net::HTTP.new(uri.host, uri.port)
48
-
49
- if uri.scheme == 'https'
50
- http.use_ssl = true
51
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
52
- else
53
- MingleEvents.log.warn BASIC_AUTH_HTTP_WARNING
54
- end
55
-
56
- path = uri.path
57
- path += "?#{uri.query}" if uri.query
58
- MingleEvents.log.info "Fetching page at #{path}..."
59
-
60
- start = Time.now
61
- req = Net::HTTP::Get.new(path)
62
- req.basic_auth(@username, @password)
63
- rsp = http.request(req)
64
- MingleEvents.log.info "...#{path} fetched in #{Time.now - start} seconds."
65
31
 
66
- rsp
32
+ private
33
+ def basic_encode(account, password)
34
+ 'Basic ' + ["#{account}:#{password}"].pack('m').delete("\r\n")
67
35
  end
68
-
69
36
  end
70
- end
37
+ end
@@ -0,0 +1,22 @@
1
+ module MingleEvents
2
+ # Client for Mingle's experimental HMAC api auth support
3
+ class MingleHmacAuthAccess
4
+ attr_reader :base_url
5
+
6
+ def initialize(base_url, login, api_key, http=Http)
7
+ @base_url = base_url
8
+ @login = login
9
+ @api_key = api_key
10
+ @http = http
11
+ end
12
+
13
+ def fetch_page(location)
14
+ location = @base_url + location if location[0..0] == '/'
15
+ @http.get(location) do |req|
16
+ ApiAuth.sign!(req, @login, @api_key)
17
+ end
18
+ end
19
+ end
20
+
21
+
22
+ end
@@ -1,40 +1,20 @@
1
- module MingleEvents
2
-
1
+ module MingleEvents
2
+
3
3
  # Client for Mingle's experimental OAuth 2.0 support in 3.0
4
- #--
5
- # TODO: Update error handling and support of fetching response
6
- # objects to that of MingleBasicAuthAccess
7
4
  class MingleOauthAccess
5
+ attr_reader :base_url
8
6
 
9
- def initialize(base_url, token)
7
+ def initialize(base_url, token, http=Http)
10
8
  @base_url = base_url
11
9
  @token = token
10
+ @http = http
12
11
  end
13
12
 
14
13
  def fetch_page(location)
15
- location = @base_url + location if location[0..0] == '/'
16
-
17
- uri = URI.parse(location)
18
- http = Net::HTTP.new(uri.host, uri.port)
19
- http.use_ssl = true
20
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
21
- headers = {
22
- 'Authorization' => %{Token token="#{@token}"}
23
- }
24
-
25
- path = uri.path
26
- path += "?#{uri.query}" if uri.query
27
- MingleEvents.log.info "Fetching page at #{path}..."
28
-
29
- start = Time.now
30
- response = http.get(path, headers)
31
- MingleEvents.log.info "... #{path} fetched in #{Time.now - start} seconds."
32
-
33
- # todo: what's the right way to raise on non 200 ?
34
- # raise StandardError.new(response.body) unless response == Net::HTTPSuccess
35
-
36
- response.body
14
+ location = @base_url + location if location[0..0] == '/'
15
+ @http.get(location) do |req|
16
+ req['Authorization'] = %{Token token="#{@token}"}
17
+ end
37
18
  end
38
-
39
19
  end
40
- end
20
+ end
@@ -5,17 +5,18 @@ module MingleEvents
5
5
  # configured for specified mingle projects. processors_by_project_identifier should
6
6
  # be a hash where the keys are mingle project identifiers and the values are
7
7
  # lists of event processors.
8
- def initialize(mingle_access, processors_by_project_identifier)
8
+ def initialize(mingle_access, processors_by_project_identifier, state_dir=nil)
9
9
  @mingle_access = mingle_access
10
10
  @processors_by_project_identifier = processors_by_project_identifier
11
+ @state_dir = state_dir
11
12
  end
12
13
 
13
14
  # Run a single poll for each project configured with processor(s) and
14
15
  # broadcast each event to each processor.
15
- def run_once(options={})
16
+ def run_once
16
17
  MingleEvents.log.info("MingleEvents::Poller about to poll once...")
17
18
  @processors_by_project_identifier.each do |project_identifier, processors|
18
- fetcher = ProjectEventFetcher.new(project_identifier, @mingle_access)
19
+ fetcher = ProjectEventFetcher.new(project_identifier, @mingle_access, @state_dir)
19
20
  fetcher.set_current_state_to_now_if_no_current_state
20
21
  latest_events = fetcher.fetch_latest.to_a
21
22
  processors.each{|p| p.process_events(latest_events)}
@@ -36,14 +36,14 @@ module MingleEvents
36
36
 
37
37
  def lookup_author_uri
38
38
  team_resource = "/api/v2/projects/#{@project_identifier}/team.xml"
39
- @raw_xml ||= @mingle_access.fetch_page(URI.escape(team_resource))
40
- @doc ||= Nokogiri::XML(@raw_xml)
39
+ @raw_xml ||= @mingle_access.fetch_page(URIParser.escape(team_resource))
40
+ @doc ||= Xml.parse(@raw_xml)
41
41
 
42
- users = @doc.search('/projects_members/projects_member/user').map do |user|
42
+ users = @doc.select_all('/projects_members/projects_member/user').map do |user|
43
43
  {
44
- :url => user.attribute('url').inner_text,
45
- :login => user.at('login').inner_text,
46
- :email => user.at('email').inner_text
44
+ :url => user.attr('url'),
45
+ :login => user.inner_text('login'),
46
+ :email => user.inner_text('email')
47
47
  }
48
48
  end
49
49
 
@@ -59,4 +59,4 @@ module MingleEvents
59
59
 
60
60
  end
61
61
  end
62
- end
62
+ end
@@ -52,7 +52,7 @@ module MingleEvents
52
52
  # if there's a real problem. In most polling scenarios, this is a highly unlikely
53
53
  # problem as there will usually be 1 or a few events.
54
54
  begin
55
- raw_xml = @mingle_access.fetch_page(URI.escape(path))
55
+ raw_xml = @mingle_access.fetch_page(URIParser.escape(path))
56
56
  rescue
57
57
  msg = %{
58
58
 
@@ -71,21 +71,21 @@ Stack Trace:
71
71
  MingleEvents.log.info(msg)
72
72
  return
73
73
  end
74
- doc = Nokogiri::XML(raw_xml)
74
+ doc = Xml.parse(raw_xml)
75
75
 
76
- doc.search('/results/result').map do |card_result|
77
- card_number = card_result.at('number').inner_text.to_i
78
- card_version = card_result.at('version').inner_text.to_i
76
+ doc.select_all('/results/result').map do |card_result|
77
+ card_number = card_result.inner_text('number').to_i
78
+ card_version = card_result.inner_text('version').to_i
79
79
  custom_properties = {}
80
80
  @card_data_by_number_and_version[data_key(card_number, card_version)] = {
81
81
  :number => card_number,
82
82
  :version => card_version,
83
- :card_type_name => card_result.at('card_type_name').inner_text,
83
+ :card_type_name => card_result.inner_text('card_type_name'),
84
84
  :custom_properties => custom_properties
85
85
  }
86
86
  card_result.children.each do |child|
87
- if child.name.index("cp_") == 0
88
- custom_properties[@custom_properties.property_name_for_column(child.name)] =
87
+ if child.tag_name.index("cp_") == 0
88
+ custom_properties[@custom_properties.property_name_for_column(child.tag_name)] =
89
89
  nullable_value_from_element(child)
90
90
  end
91
91
  end
@@ -95,17 +95,17 @@ Stack Trace:
95
95
  def load_card_data_for_event(card_event)
96
96
  begin
97
97
  page_xml = @mingle_access.fetch_page(card_event.card_version_resource_uri)
98
- doc = Nokogiri::XML(page_xml)
98
+ doc = Xml.parse(page_xml)
99
99
  custom_properties = {}
100
100
  result = {
101
101
  :number => card_event.card_number,
102
102
  :version => card_event.version,
103
- :card_type_name => doc.at('/card/card_type/name').inner_text,
103
+ :card_type_name => doc.select('/card/card_type/name').inner_text,
104
104
  :custom_properties => custom_properties
105
105
  }
106
- doc.search('/card/properties/property').each do |property|
107
- custom_properties[property.at('name').inner_text] =
108
- nullable_value_from_element(property.at('value'))
106
+ doc.select_all('/card/properties/property').each do |property|
107
+ custom_properties[property.inner_text('name')] =
108
+ nullable_value_from_element(property.select('value'))
109
109
  end
110
110
 
111
111
  result
@@ -116,9 +116,9 @@ Stack Trace:
116
116
  end
117
117
 
118
118
  def nullable_value_from_element(element)
119
- element['nil'] == 'true' ? nil : element.inner_text
119
+ element.attr('nil') == 'true' ? nil : element.inner_text
120
120
  end
121
121
  end
122
122
 
123
123
  end
124
- end
124
+ end
@@ -1,33 +1,33 @@
1
1
  module MingleEvents
2
-
2
+
3
3
  class ProjectCustomProperties
4
-
4
+
5
5
  def initialize(mingle_access, project_identifier)
6
6
  @mingle_access = mingle_access
7
7
  @project_identifier = project_identifier
8
8
  end
9
-
9
+
10
10
  def property_name_for_column(column_name)
11
11
  property_names_by_column_name[column_name]
12
12
  end
13
-
13
+
14
14
  private
15
-
15
+
16
16
  def property_names_by_column_name
17
17
  @property_names_by_column_name ||= lookup_property_names_by_column_name
18
18
  end
19
-
20
- def lookup_property_names_by_column_name
21
- as_document.search('/property_definitions/property_definition').inject({}) do |mapping, element|
22
- mapping[element.at('column_name').inner_text] = element.at('name').inner_text
19
+
20
+ def lookup_property_names_by_column_name
21
+ as_document.select_all('/property_definitions/property_definition').inject({}) do |mapping, element|
22
+ mapping[element.inner_text('column_name')] = element.inner_text('name')
23
23
  mapping
24
24
  end
25
25
  end
26
-
26
+
27
27
  def as_document
28
- @as_document ||= Nokogiri::XML(@mingle_access.fetch_page("/api/v2/projects/#{@project_identifier}/property_definitions.xml"))
29
- end
30
-
28
+ @as_document ||= Xml.parse(@mingle_access.fetch_page("/api/v2/projects/#{@project_identifier}/property_definitions.xml"))
29
+ end
30
+
31
31
  end
32
-
33
- end
32
+
33
+ end
@@ -7,160 +7,71 @@ module MingleEvents
7
7
  # one at at time, reading it off disk, to avoid massive memory consumption.
8
8
  class ProjectEventFetcher
9
9
 
10
+ attr_reader :entry_cache
11
+
10
12
  def initialize(project_identifier, mingle_access, state_dir=nil)
11
13
  @project_identifier = project_identifier
12
14
  @mingle_access = mingle_access
13
15
  base_uri = URI.parse(mingle_access.base_url)
14
- @state_dir = File.expand_path(state_dir || File.join('~', '.mingle_events', base_uri.host, base_uri.port.to_s, project_identifier, 'fetched_events'))
16
+ @state_dir = state_dir || File.join('~', '.mingle_events', base_uri.host, base_uri.port.to_s)
17
+ @state_dir = File.expand_path(File.join(@state_dir, project_identifier, 'fetched_events'))
18
+ @entry_cache = EntryCache.new(@state_dir)
15
19
  end
16
20
 
17
21
  # blow away any existing state, when next used to fetch events from mingle
18
22
  # will crawl all the way back to time zero
19
23
  def reset
20
- FileUtils.rm_rf(@state_dir)
24
+ @entry_cache.clear
21
25
  end
22
26
 
23
27
  def set_current_state_to_now_if_no_current_state
24
- return if has_current_state?
25
-
26
- latest_event = page_with_latest_entries.entries.first
27
- return if latest_event.nil?
28
- write_entry_to_disk(latest_event, nil)
29
- update_current_state(latest_event, latest_event)
28
+ return if @entry_cache.has_current_state?
29
+ @entry_cache.set_current_state(page_with_latest_entries.entries.first)
30
30
  end
31
31
 
32
32
  # fetch the latest events from mingle, i.e., the ones not previously seen
33
33
  def fetch_latest
34
34
  page = page_with_latest_entries
35
35
  most_recent_new_entry = page.entries.first
36
- last_fetched_entry = load_last_fetched_entry
36
+ last_fetched_entry = @entry_cache.latest
37
37
  last_fetched_entry_seen = false
38
38
  next_entry = nil
39
39
  while !last_fetched_entry_seen && page
40
40
  page.entries.each do |entry|
41
41
 
42
+ @entry_cache.write(entry, next_entry)
42
43
  if last_fetched_entry && entry.entry_id == last_fetched_entry.entry_id
43
44
  last_fetched_entry_seen = true
44
45
  break
45
46
  end
46
-
47
- write_entry_to_disk(entry, next_entry)
47
+
48
48
  next_entry = entry
49
-
50
49
  end
51
50
  page = page.next
52
51
  end
53
52
 
54
- update_current_state(next_entry, most_recent_new_entry)
55
- Entries.new(file_for_entry(next_entry), file_for_entry(most_recent_new_entry))
53
+ @entry_cache.update_current_state(next_entry, most_recent_new_entry)
54
+ @entry_cache.entries(next_entry, most_recent_new_entry)
56
55
  end
57
56
 
58
57
  # returns all previously fetched entries; can be used to reprocess the events for, say,
59
58
  # various historical analyses
60
59
  def all_fetched_entries
61
- current_state = load_current_state
62
- Entries.new(current_state[:first_fetched_entry_info_file], current_state[:last_fetched_entry_info_file])
63
- end
64
-
65
- def first_entry_fetched
66
- current_state_entry(:first_fetched_entry_info_file)
60
+ @entry_cache.all_entries
67
61
  end
68
62
 
69
- def last_entry_fetched
70
- current_state_entry(:last_fetched_entry_info_file)
63
+ def first_entry_fetched
64
+ @entry_cache.first
71
65
  end
72
66
 
73
- # only public to facilitate testing
74
- def update_current_state(oldest_new_entry, most_recent_new_entry)
75
- current_state = load_current_state
76
- # if most_recent_new_entry
77
- current_state.merge!(:last_fetched_entry_info_file => file_for_entry(most_recent_new_entry))
78
- if current_state[:first_fetched_entry_info_file].nil?
79
- current_state.merge!(:first_fetched_entry_info_file => file_for_entry(oldest_new_entry))
80
- end
81
- File.open(current_state_file, 'w'){|out| YAML.dump(current_state, out)}
82
- # end
83
- end
84
-
85
- # only public to facilitate testing
86
- def write_entry_to_disk(entry, next_entry)
87
- file = file_for_entry(entry)
88
- FileUtils.mkdir_p(File.dirname(file))
89
- file_content = {:entry_xml => entry.raw_xml, :next_entry_file_path => file_for_entry(next_entry)}
90
- File.open(file, 'w'){|out| YAML.dump(file_content, out)}
67
+ def last_entry_fetched
68
+ @entry_cache.latest
91
69
  end
92
-
70
+
93
71
  private
94
72
 
95
73
  def page_with_latest_entries
96
74
  Feed::Page.new("/api/v2/projects/#{@project_identifier}/feeds/events.xml", @mingle_access)
97
- end
98
-
99
- def current_state_entry(info_file_key)
100
- if info_file = load_current_state[info_file_key]
101
- Feed::Entry.from_snippet(YAML.load(File.new(info_file))[:entry_xml])
102
- else
103
- nil
104
- end
105
- end
106
-
107
- def file_for_entry(entry)
108
- return nil if entry.nil?
109
-
110
- entry_id_as_uri = URI.parse(entry.entry_id)
111
- relative_path_parts = entry_id_as_uri.path.split('/')
112
- entry_id_int = relative_path_parts.last
113
- insertions = ["#{entry_id_int.to_i/16384}", "#{entry_id_int.to_i%16384}"]
114
- relative_path_parts = relative_path_parts[0..-2] + insertions + ["#{entry_id_int}.yml"]
115
- File.join(@state_dir, *relative_path_parts)
116
- end
117
-
118
- def has_current_state?
119
- File.exist?(current_state_file)
120
- end
121
-
122
- def current_state_file
123
- File.expand_path(File.join(@state_dir, 'current_state.yml'))
124
- end
125
-
126
- def load_last_fetched_entry
127
- current_state = load_current_state
128
- last_fetched_entry = if current_state[:last_fetched_entry_info_file]
129
- last_fetched_entry_info = YAML.load(File.new(current_state[:last_fetched_entry_info_file]))
130
- Feed::Entry.from_snippet(last_fetched_entry_info[:entry_xml])
131
- else
132
- nil
133
- end
134
- end
135
-
136
- def load_current_state
137
- if has_current_state?
138
- YAML.load(File.new(current_state_file))
139
- else
140
- {:last_fetched_entry_info_file => nil, :first_fetched_entry_info_file => nil}
141
- end
142
- end
143
-
144
- class Entries
145
-
146
- include Enumerable
147
-
148
- def initialize(first_info_file, last_info_file)
149
- @first_info_file = first_info_file
150
- @last_info_file = last_info_file
151
- end
152
-
153
- def each(&block)
154
- current_file = @first_info_file
155
- while current_file
156
- current_entry_info = YAML.load(File.new(current_file))
157
- yield(Feed::Entry.from_snippet(current_entry_info[:entry_xml]))
158
- break if File.expand_path(current_file) == File.expand_path(@last_info_file)
159
- current_file = current_entry_info[:next_entry_file_path]
160
- end
161
- end
162
-
163
- end
164
-
75
+ end
165
76
  end
166
- end
77
+ end