apple_epf 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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