apple_epf 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.
Files changed (30) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +90 -0
  3. data/Rakefile +35 -0
  4. data/lib/apple_epf.rb +43 -0
  5. data/lib/apple_epf/downloader.rb +164 -0
  6. data/lib/apple_epf/errors.rb +8 -0
  7. data/lib/apple_epf/extractor.rb +43 -0
  8. data/lib/apple_epf/logging.rb +32 -0
  9. data/lib/apple_epf/main.rb +113 -0
  10. data/lib/apple_epf/parser.rb +95 -0
  11. data/lib/apple_epf/version.rb +3 -0
  12. data/lib/core_ext/array.rb +30 -0
  13. data/lib/core_ext/module.rb +63 -0
  14. data/lib/tasks/apple_epf_tasks.rake +4 -0
  15. data/spec/lib/apple_epf/downloader_spec.rb +220 -0
  16. data/spec/lib/apple_epf/exctractor_spec.rb +72 -0
  17. data/spec/lib/apple_epf/main_spec.rb +185 -0
  18. data/spec/lib/apple_epf/parser_spec.rb +66 -0
  19. data/spec/spec_helper.rb +40 -0
  20. data/spec/support/fixture_helper.rb +13 -0
  21. data/spec/support/fixtures/itunes/epf/current_full_list.html +20 -0
  22. data/spec/support/fixtures/itunes/epf/current_inc_list.html +19 -0
  23. data/spec/support/fixtures/itunes/epf/incremental/itunes20130111.tbz +0 -0
  24. data/spec/support/fixtures/itunes/epf/incremental/itunes20130111/application +21 -0
  25. data/spec/support/fixtures/itunes/epf/incremental/itunes20130111/application_with_nil +7 -0
  26. data/spec/support/fixtures/itunes/epf/incremental/itunes20130111/test_file.txt +0 -0
  27. data/spec/support/fixtures/itunes/epf/incremental/popularity20130111.tbz +0 -0
  28. data/spec/support/fixtures/itunes/epf/incremental/popularity20130111/popularity1 +0 -0
  29. data/spec/support/fixtures/itunes/epf/incremental/popularity20130111/popularity2 +0 -0
  30. metadata +255 -0
@@ -0,0 +1,95 @@
1
+ module AppleEpf
2
+ class Parser
3
+ FIELD_SEPARATOR = 1.chr
4
+ RECORD_SEPARATOR = 2.chr + "\n"
5
+ COMMENT_CHAR = '#'
6
+
7
+ attr_accessor :filename, :header_info, :footer_info
8
+
9
+ def initialize(filename)
10
+ @filename = filename
11
+ @header_info = {}
12
+ @footer_info = {}
13
+ end
14
+
15
+ def parse_metadata
16
+ begin
17
+ parse_file
18
+ load_header_info
19
+ load_footer_info
20
+ @header_info.merge(@footer_info)
21
+ ensure
22
+ close_file
23
+ end
24
+ end
25
+
26
+ def process_rows(&block)
27
+ File.foreach( @filename, RECORD_SEPARATOR ) do |line|
28
+ unless line[0].chr == COMMENT_CHAR
29
+ line = line.chomp( RECORD_SEPARATOR )
30
+ block.call( line.split( FIELD_SEPARATOR, -1) ) if block_given?
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def parse_file
38
+ @file = File.new( @filename, 'r', encoding: 'UTF-8' )
39
+ end
40
+
41
+ def close_file
42
+ @file.close if @file
43
+ end
44
+
45
+ def read_line(accept_comment = false)
46
+ valid_line = false
47
+ until valid_line
48
+ begin
49
+ line = @file.readline( RECORD_SEPARATOR )
50
+ rescue EOFError => e
51
+ return nil
52
+ end
53
+ valid_line = accept_comment ? true : !line.start_with?( COMMENT_CHAR )
54
+ end
55
+ line.sub!( COMMENT_CHAR, '' ) if accept_comment
56
+ line.chomp!( RECORD_SEPARATOR )
57
+ end
58
+
59
+ def load_header_info
60
+ # File
61
+ file_hash = { :file => File.basename( @filename ) }
62
+ @header_info.merge! ( file_hash )
63
+
64
+ # Columns
65
+ line = read_line(true)
66
+ column_hash = { :columns => line.split( FIELD_SEPARATOR ) }
67
+ @header_info.merge! ( column_hash )
68
+
69
+ # Primary keys
70
+ line = read_line(true).sub!( 'primaryKey:', '' )
71
+ primary_hash = { :primary_keys => line.split( FIELD_SEPARATOR ) }
72
+ @header_info.merge! ( primary_hash )
73
+
74
+ # DB types
75
+ line = read_line(true).sub!( 'dbTypes:', '' )
76
+ primary_hash = { :db_types => line.split( FIELD_SEPARATOR ) }
77
+ @header_info.merge! ( primary_hash )
78
+
79
+ # Export type
80
+ line = read_line(true).sub!( 'exportMode:', '' )
81
+ primary_hash = { :export_type => line.split( FIELD_SEPARATOR ) }
82
+ @header_info.merge! ( primary_hash )
83
+ end
84
+
85
+
86
+ def load_footer_info
87
+ @file.seek(-40, IO::SEEK_END)
88
+ records = @file.read.split( COMMENT_CHAR ).last.chomp!( RECORD_SEPARATOR ).sub( 'recordsWritten:', '' )
89
+ records_hash = { :records => records }
90
+ @footer_info.merge! ( records_hash )
91
+ @file.rewind
92
+ @footer_info
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,3 @@
1
+ module AppleEpf
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,30 @@
1
+ # activesupport/lib/active_support/core_ext/array/extract_options.rb
2
+ class Hash
3
+ # By default, only instances of Hash itself are extractable.
4
+ # Subclasses of Hash may implement this method and return
5
+ # true to declare themselves as extractable. If a Hash
6
+ # is extractable, Array#extract_options! pops it from
7
+ # the Array when it is the last element of the Array.
8
+ def extractable_options?
9
+ instance_of?(Hash)
10
+ end
11
+ end
12
+
13
+ class Array
14
+ # Extracts options from a set of arguments. Removes and returns the last
15
+ # element in the array if it's a hash, otherwise returns a blank hash.
16
+ #
17
+ # def options(*args)
18
+ # args.extract_options!
19
+ # end
20
+ #
21
+ # options(1, 2) # => {}
22
+ # options(1, 2, :a => :b) # => {:a=>:b}
23
+ def extract_options!
24
+ if last.is_a?(Hash) && last.extractable_options?
25
+ pop
26
+ else
27
+ {}
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,63 @@
1
+ # activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
2
+ class Module
3
+ def mattr_reader(*syms)
4
+ options = syms.extract_options!
5
+ syms.each do |sym|
6
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
7
+ @@#{sym} = nil unless defined? @@#{sym}
8
+
9
+ def self.#{sym}
10
+ @@#{sym}
11
+ end
12
+ EOS
13
+
14
+ unless options[:instance_reader] == false || options[:instance_accessor] == false
15
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
16
+ def #{sym}
17
+ @@#{sym}
18
+ end
19
+ EOS
20
+ end
21
+ end
22
+ end
23
+
24
+ def mattr_writer(*syms)
25
+ options = syms.extract_options!
26
+ syms.each do |sym|
27
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
28
+ def self.#{sym}=(obj)
29
+ @@#{sym} = obj
30
+ end
31
+ EOS
32
+
33
+ unless options[:instance_writer] == false || options[:instance_accessor] == false
34
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
35
+ def #{sym}=(obj)
36
+ @@#{sym} = obj
37
+ end
38
+ EOS
39
+ end
40
+ end
41
+ end
42
+
43
+ # Extends the module object with module and instance accessors for class attributes,
44
+ # just like the native attr* accessors for instance attributes.
45
+ #
46
+ # module AppConfiguration
47
+ # mattr_accessor :google_api_key
48
+ # self.google_api_key = "123456789"
49
+ #
50
+ # mattr_accessor :paypal_url
51
+ # self.paypal_url = "www.sandbox.paypal.com"
52
+ # end
53
+ #
54
+ # AppConfiguration.google_api_key = "overriding the api key!"
55
+ #
56
+ # To opt out of the instance writer method, pass :instance_writer => false.
57
+ # To opt out of the instance reader method, pass :instance_reader => false.
58
+ # To opt out of both instance methods, pass :instance_accessor => false.
59
+ def mattr_accessor(*syms)
60
+ mattr_reader(*syms)
61
+ mattr_writer(*syms)
62
+ end
63
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :apple_epf do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,220 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path('../../../spec_helper', __FILE__)
3
+
4
+ describe AppleEpf::Downloader do
5
+
6
+ let(:type) { 'incremental' }
7
+ let(:filedate) { Date.parse('17-01-2013') }
8
+ let(:file) { 'popularity' }
9
+ let(:downloader) {AppleEpf::Downloader.new(type, file, filedate)}
10
+ let(:file_exists) { true }
11
+
12
+ describe "#get_filename_by_date_and_type" do
13
+ before do
14
+ downloader.stub(:file_exists?){ file_exists }
15
+ end
16
+
17
+ it "should raise exception if path can not be determined" do
18
+ downloader.type = 'crazytype'
19
+ expect {
20
+ downloader.get_filename_by_date_and_type
21
+ }.to raise_exception
22
+ end
23
+
24
+ context "type is full" do
25
+ let(:type) { 'full' }
26
+
27
+ context "and file exists" do
28
+ let(:file_exists) { true }
29
+
30
+ it "should return valid url if file exists" do
31
+ downloader.filedate = Date.parse('17-01-2013')
32
+ downloader.get_filename_by_date_and_type.should == "20130116/popularity20130116.tbz"
33
+ end
34
+ end
35
+
36
+ context "and file does not exists" do
37
+ let(:file_exists) { false }
38
+
39
+ it "should raise exception" do
40
+ downloader.filedate = Date.parse('17-01-2013')
41
+ expect {
42
+ downloader.get_filename_by_date_and_type
43
+ }.to raise_exception(AppleEpf::FileNotExist)
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ context "type is incremental" do
50
+ let(:type) { 'incremental' }
51
+
52
+ context "and file exists" do
53
+ let(:file_exists) { true }
54
+
55
+ it "should return valid url if file exists" do
56
+ downloader.filedate = Date.parse('17-01-2013')
57
+ downloader.get_filename_by_date_and_type.should == "20130109/incremental/20130117/popularity20130117.tbz"
58
+ end
59
+ end
60
+
61
+ context "and file does not exists" do
62
+ let(:file_exists) { false }
63
+
64
+ it "should raise exception" do
65
+ downloader.filedate = Date.parse('17-01-2013')
66
+ expect {
67
+ downloader.get_filename_by_date_and_type
68
+ }.to raise_exception(AppleEpf::FileNotExist)
69
+ end
70
+ end
71
+
72
+ end
73
+
74
+ context "type is file" do
75
+ pending
76
+ end
77
+
78
+ end
79
+
80
+ describe "#main_dir_date" do
81
+
82
+ context "full" do
83
+ let(:type) { 'full' }
84
+ it "should return the same week wednesday" do
85
+ downloader.filedate = Date.parse('17-01-2013') #thursday
86
+ downloader.send(:main_dir_date).should == "20130116"
87
+ end
88
+ # it "should return wednesday of this week if filedate is thur-sun" do
89
+ # downloader.filedate = Date.parse('17-01-2013') #thursday
90
+ # downloader.send(:main_dir_date).should == "20130116"
91
+
92
+ # downloader.filedate = Date.parse('19-01-2013') #sut
93
+ # downloader.send(:main_dir_date).should == "20130116"
94
+ # end
95
+
96
+ # it "should return wednesday of prev week if filedate is mon-wed" do
97
+ # downloader.filedate = Date.parse('21-01-2013') #monday
98
+ # downloader.send(:main_dir_date).should == "20130123"
99
+
100
+ # downloader.filedate = Date.parse('23-01-2013') #wednesday
101
+ # downloader.send(:main_dir_date).should == "20130123"
102
+ # end
103
+ end
104
+
105
+ context "incremental" do
106
+ let(:type) { 'incremental' }
107
+ it "should return wednesday of this week if filedate is friday-sunday" do
108
+ downloader.filedate = Date.parse('18-01-2013') #friday
109
+ downloader.send(:main_dir_date).should == "20130116"
110
+
111
+ downloader.filedate = Date.parse('19-01-2013') #sut
112
+ downloader.send(:main_dir_date).should == "20130116"
113
+ end
114
+
115
+ it "should return wednesday of prev week if filedate is monday-thursday" do
116
+ downloader.filedate = Date.parse('21-01-2013') #monday
117
+ downloader.send(:main_dir_date).should == "20130116"
118
+
119
+ downloader.filedate = Date.parse('24-01-2013') #thursday
120
+ downloader.send(:main_dir_date).should == "20130116"
121
+ end
122
+ end
123
+ end
124
+
125
+ describe "download" do
126
+ let(:filedate) { Date.parse('21-01-2013') }
127
+
128
+ before do
129
+ @tmp_dir = [Dir.tmpdir, 'epm_files'].join('/')
130
+ FileUtils.mkpath @tmp_dir
131
+
132
+ AppleEpf.configure do |config|
133
+ config.apple_id = 'test'
134
+ config.apple_password = 'test'
135
+ config.extract_dir = @tmp_dir
136
+ end
137
+
138
+ downloader.stub(:download_and_compare_md5_checksum)
139
+ end
140
+
141
+ it "should properly set url for download" do
142
+ downloader.stub(:file_exists?){ file_exists }
143
+ downloader.stub(:start_download)
144
+ downloader.download
145
+ downloader.apple_filename_full.should eq("http://feeds.itunes.apple.com/feeds/epf/v3/full/20130116/incremental/20130121/popularity20130121.tbz")
146
+ end
147
+
148
+ it "should properly set local file to store file in" do
149
+ downloader.stub(:file_exists?){ file_exists }
150
+ downloader.stub(:start_download)
151
+ downloader.download
152
+ downloader.download_to.should eq("#{@tmp_dir}/incremental/popularity20130121.tbz")
153
+ end
154
+
155
+ it "should download and save file" do
156
+ stub_request(:get, "http://test:test@feeds.itunes.apple.com/feeds/epf/v3/full/20130123/popularity20130123.tbz").
157
+ to_return(:status => 200, :body => "Test\nWow", :headers => {})
158
+
159
+ downloader = AppleEpf::Downloader.new('full', file, filedate)
160
+ downloader.stub(:download_and_compare_md5_checksum)
161
+ downloader.stub(:file_exists?){ file_exists }
162
+ downloader.download
163
+ IO.read(downloader.download_to).should eq("Test\nWow")
164
+ end
165
+
166
+ it "should retry 3 times to download" do
167
+ pending
168
+ end
169
+
170
+ describe "dirpath" do
171
+ before do
172
+ downloader.stub(:file_exists?){ file_exists }
173
+ downloader.stub(:start_download)
174
+ end
175
+
176
+ it "should be able to change dir where to save files" do
177
+ tmp_dir = Dir.tmpdir
178
+ downloader.dirpath = [tmp_dir, 'whatever_path'].join('/')
179
+ downloader.download.should == "#{tmp_dir}/whatever_path/incremental/popularity20130121.tbz"
180
+ end
181
+ end
182
+
183
+ describe "#download_and_compare_md5_checksum" do
184
+ before do
185
+ downloader.unstub(:download_and_compare_md5_checksum)
186
+ end
187
+ it "should raise exception if md5 file does not match real md5 checksum of file" do
188
+ stub_request(:get, "http://test:test@feeds.itunes.apple.com/feeds/epf/v3/full/20130116/incremental/20130121/popularity20130121.tbz").
189
+ to_return(:status => 200, :body => "Test\nWow", :headers => {})
190
+
191
+ stub_request(:get, "http://test:test@feeds.itunes.apple.com/feeds/epf/v3/full/20130116/incremental/20130121/popularity20130121.tbz.md5").
192
+ to_return(:status => 200, :body => "tupo", :headers => {})
193
+
194
+ downloader.stub(:file_exists?){ file_exists }
195
+
196
+ expect {
197
+ downloader.download
198
+ }.to raise_exception(AppleEpf::Md5CompareError)
199
+
200
+ end
201
+
202
+ it "should not raise exception if md5 is ok" do
203
+ stub_request(:get, "http://test:test@feeds.itunes.apple.com/feeds/epf/v3/full/20130116/incremental/20130121/popularity20130121.tbz").
204
+ to_return(:status => 200, :body => "Test\nWow", :headers => {})
205
+
206
+ stub_request(:get, "http://test:test@feeds.itunes.apple.com/feeds/epf/v3/full/20130116/incremental/20130121/popularity20130121.tbz.md5").
207
+ to_return(:status => 200, :body => "MD5 (popularity20130116.tbz) = 0371a79664856494e840af9e1e6c0152\n", :headers => {})
208
+
209
+
210
+ downloader.stub(:file_exists?){ file_exists }
211
+
212
+ expect {
213
+ downloader.download
214
+ }.not_to raise_exception(AppleEpf::Md5CompareError)
215
+
216
+ end
217
+ end
218
+ end
219
+
220
+ end
@@ -0,0 +1,72 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path('../../../spec_helper', __FILE__)
3
+
4
+ describe AppleEpf::Extractor do
5
+ let(:file_basename) { 'itunes20130111.tbz' }
6
+ let(:files_to_extract) { ['application', 'test_file.txt'] }
7
+
8
+ before do
9
+ @tmp_dir = [Dir.tmpdir, 'test_epm_files'].join('/')
10
+ FileUtils.mkpath @tmp_dir
11
+
12
+ AppleEpf.configure do |config|
13
+ config.apple_id = 'test'
14
+ config.apple_password = 'test'
15
+ config.extract_dir = @tmp_dir
16
+ end
17
+
18
+ @copy_to = "#{@tmp_dir}/#{file_basename}"
19
+ FileUtils.copy_file(apple_epf_inc_filename(file_basename), @copy_to)
20
+ end
21
+
22
+ after do
23
+ FileUtils.remove_dir(@tmp_dir)
24
+ end
25
+
26
+ describe "initialize" do
27
+ it "should set instance variables" do
28
+ extractor = AppleEpf::Extractor.new(@copy_to, files_to_extract)
29
+
30
+ extractor.filename.should == @copy_to
31
+ extractor.dirname.should == @tmp_dir
32
+ extractor.basename.should == file_basename
33
+ end
34
+ end
35
+
36
+ describe "perform" do
37
+ it "should raise error if extracting was not successful" do
38
+ files_to_extract = ['application', 'wrong_file.txt']
39
+ extractor = AppleEpf::Extractor.new(@copy_to, files_to_extract)
40
+
41
+ expect {
42
+ extractor.perform
43
+ }.to raise_exception ("Unable to extract files '#{files_to_extract.join(' ')}' from #{@copy_to}")
44
+ end
45
+
46
+ it "should return list if extracted files" do
47
+ extractor = AppleEpf::Extractor.new(@copy_to, files_to_extract)
48
+ extractor.perform
49
+ extractor.file_entry.tbz_file.should == @copy_to
50
+
51
+ expected_extracted = files_to_extract.map do |f|
52
+ File.join(@tmp_dir, 'itunes20130111', f)
53
+ end
54
+
55
+ extractor.file_entry.extracted_files.should == Hash[files_to_extract.zip(expected_extracted)]
56
+ extractor.file_entry.tbz_file.should == @copy_to
57
+ end
58
+
59
+ it "should remove file if successfully untarred" do
60
+ extractor = AppleEpf::Extractor.new(@copy_to, files_to_extract)
61
+ extractor.perform
62
+ File.exists?(extractor.filename).should be_false
63
+ end
64
+
65
+ it "should not remove file if successfully untarred and it was asked to leave file" do
66
+ extractor = AppleEpf::Extractor.new(@copy_to, files_to_extract)
67
+ extractor.keep_tbz_after_extract = true
68
+ extractor.perform
69
+ File.exists?(extractor.filename).should be_true
70
+ end
71
+ end
72
+ end