webget 0.3.1 → 0.3.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de0060acabe176d1a11a4f2e36e8bee5090083c4907b75d9b22e9cbb9c7b22e2
4
- data.tar.gz: fb64c2b1294932b00fa401af006cd27b263b255789bad75e4ef62bec5598fd74
3
+ metadata.gz: bb98b8d5740ff961389adbfcda8b1a461c1aa0f8fe6f9cc748ac8411c8559610
4
+ data.tar.gz: ccb159f52cc457f6a83e2fceabdab1d197018a58347e765b32726651a5985f01
5
5
  SHA512:
6
- metadata.gz: d1f1653c68729e7d609c3c848e0146de9668cd2932ddab2e794ab361d3f786603a10c7586bcf0029de4955255803987000b2fb35b0d1e14cfa35d0582f919be7
7
- data.tar.gz: c6cff08b2f683bb5b8e39607735250f954115244be3e19dfcdeb29fd2d0a0f0a27be1fed5a2add9a1b75a9cbe81dbafea1e80f06b0a1bdd93d4bd3bd12d5a9a1
6
+ metadata.gz: 1d469ba45efeaf35d95d241e0521e2b447779ac88e8d5788e5e9599a759c9d7ad0e25c9ecff0a263d2755c9d1cf757196f0511a7172d17788652ab37c1a25f45
7
+ data.tar.gz: 87122ab93ceabb553b289f3f2ebc9343ee8d063b38ddd7f142e0534543acd9bb152aa8903b94b442b5d9249a05ffaaefcdd34b2ad41701540d0970d428551d0d
data/CHANGELOG.md CHANGED
@@ -1,6 +1,4 @@
1
- ### 0.3.1
2
-
1
+ ### 0.3.3
3
2
  ### 0.0.1 / 2020-10-04
4
3
 
5
4
  * Everything is new. First release.
6
-
data/Manifest.txt CHANGED
@@ -5,4 +5,7 @@ Rakefile
5
5
  lib/webget.rb
6
6
  lib/webget/version.rb
7
7
  lib/webget/webcache.rb
8
+ lib/webget/webcache_disk-rewrite.rb
9
+ lib/webget/webcache_disk.rb
10
+ lib/webget/webcache_headers.rb
8
11
  lib/webget/webget.rb
data/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  webget gem - a web (go get) crawler incl. web cache
4
4
 
5
- * home :: [github.com/rubycoco/webclient](https://github.com/rubycoco/webclient)
6
- * bugs :: [github.com/rubycoco/webclient/issues](https://github.com/rubycoco/webclient/issues)
5
+ * home :: [github.com/rubycocos/webclient](https://github.com/rubycocos/webclient)
6
+ * bugs :: [github.com/rubycocos/webclient/issues](https://github.com/rubycocos/webclient/issues)
7
7
  * gem :: [rubygems.org/gems/webget](https://rubygems.org/gems/webget)
8
8
  * rdoc :: [rubydoc.info/gems/webget](http://rubydoc.info/gems/webget)
9
9
 
@@ -16,4 +16,4 @@ TBD
16
16
  ## License
17
17
 
18
18
  The `webget` scripts are dedicated to the public domain.
19
- Use it as you please with no restrictions whatsoever.
19
+ Use as you please with no restrictions whatsoever.
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ Hoe.spec 'webget' do
8
8
  self.summary = 'webget gem - a web (go get) crawler incl. web cache'
9
9
  self.description = summary
10
10
 
11
- self.urls = { home: 'https://github.com/rubycoco/webclient' }
11
+ self.urls = { home: 'https://github.com/rubycocos/webclient' }
12
12
 
13
13
  self.author = 'Gerald Bauer'
14
14
  self.email = 'gerald.bauer@gmail.com'
@@ -18,15 +18,14 @@ Hoe.spec 'webget' do
18
18
  self.history_file = 'CHANGELOG.md'
19
19
 
20
20
  self.extra_deps = [
21
- ['webclient', '>= 0.2.0'],
22
- ['csvreader', '>= 1.2.4'],
21
+ ['webclient', '>= 0.3.1'],
23
22
  ['cocos'],
24
23
  ]
25
24
 
26
25
  self.licenses = ['Public Domain']
27
26
 
28
27
  self.spec_extras = {
29
- required_ruby_version: '>= 2.2.2'
28
+ required_ruby_version: '>= 3.1.0'
30
29
  }
31
30
 
32
31
  end
@@ -2,7 +2,7 @@
2
2
  class Webget
3
3
  MAJOR = 0 ## todo: namespace inside version or something - why? why not??
4
4
  MINOR = 3
5
- PATCH = 1
5
+ PATCH = 3
6
6
  VERSION = [MAJOR,MINOR,PATCH].join('.')
7
7
 
8
8
  def self.version
@@ -18,4 +18,3 @@ class Webget
18
18
  File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )
19
19
  end
20
20
  end # module Webget
21
-
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Webcache
4
4
 
5
+
5
6
  #####
6
7
  # copied from props gem, see Env.home
7
8
  # - https://github.com/rubycoco/props/blob/master/props/lib/props/env.rb
@@ -26,7 +27,6 @@ module Webcache
26
27
  ## note: use File.expand_path to "unify" path e.g
27
28
  ## C:\Users\roman becomes
28
29
  ## C:/Users/roman
29
-
30
30
  File.expand_path( path )
31
31
  end
32
32
 
@@ -34,7 +34,14 @@ module Webcache
34
34
  class Configuration
35
35
  ## root directory - todo/check: find/use a better name - why? why not?
36
36
  def root() @root || "#{Webcache.home}/.cache"; end
37
- def root=(value) @root = value; end
37
+ def root=(path)
38
+ ## note: use File.expand_path to "unify" and auto-expand path
39
+ ## to make sure always absolute
40
+ ## e.g
41
+ ## C:\Users\roman becomes
42
+ ## C:/Users/roman
43
+ @root = File.expand_path(path)
44
+ end
38
45
  end # class Configuration
39
46
 
40
47
 
@@ -49,186 +56,51 @@ module Webcache
49
56
  ## add "high level" root convenience helpers
50
57
  ## use delegate helper - why? why not?
51
58
  def self.root() config.root; end
52
- def self.root=(value) config.root = value; end
59
+ def self.root=(path) config.root = path; end
53
60
 
54
61
 
55
- ### "interface" for "generic" cache storage (might be sqlite database or filesystem)
56
- def self.cache() @cache ||= DiskCache.new; end
57
-
58
- def self.record( url, response,
59
- path: nil,
60
- encoding: 'UTF-8',
61
- format: 'html' )
62
- cache.record( url, response,
63
- path: path,
64
- encoding: encoding,
65
- format: format );
66
- end
67
- def self.cached?( url ) cache.cached?( url ); end
68
- class << self
69
- alias_method :exist?, :cached?
70
- end
71
- def self.url_to_id( url ) cache.url_to_id( url ); end ## todo/check: rename to just id or something - why? why not?
72
- def self.read( url ) cache.read( url ); end
73
- def self.read_json( url ) cache.read_json( url ); end
74
- def self.read_csv( url ) cache.read_csv( url ); end
75
62
 
76
63
 
77
64
 
78
- class DiskCache
79
- def cached?( url )
80
- body_path = "#{Webcache.root}/#{url_to_path( url )}"
81
- File.exist?( body_path )
82
- end
83
- alias_method :exist?, :cached?
65
+ ### "interface" for "generic" cache storage (might be sqlite database or filesystem)
66
+ def self.cache() @cache ||= DiskCache.new; end
84
67
 
68
+ def self.record( url, response, format: ) ## html|txt|csv|json|etc.
69
+ ## note - (text) encoding_user MUST get passed along in response obj/wrapper
70
+ ## response._encoding_user = encoding! !!
71
+ cache.record( url, response, format: format );
72
+ end
85
73
 
86
- def read( url )
87
- body_path = "#{Webcache.root}/#{url_to_path( url )}"
88
- File.open( body_path, 'r:utf-8' ) {|f| f.read }
89
- end
74
+ def self.cached?( url ) cache.cached?( url ); end
90
75
 
91
- def read_json( url )
92
- body_path = "#{Webcache.root}/#{url_to_path( url )}"
93
- txt = File.open( body_path, 'r:utf-8' ) {|f| f.read }
94
- data = JSON.parse( txt )
95
- data
96
- end
76
+ ## def self.url_to_id( url ) cache.url_to_id( url ); end ## todo/check: rename to just id or something - why? why not?
97
77
 
98
- def read_csv( url )
99
- body_path = "#{Webcache.root}/#{url_to_path( url )}"
100
- txt = File.open( body_path, 'r:utf-8' ) {|f| f.read }
101
- data = CsvHash.parse( txt )
102
- data
103
- end
104
78
 
79
+ def self.read( url ) cache.read( url ); end
80
+ def self.read_json( url ) cache.read_json( url ); end
81
+ def self.read_csv( url ) cache.read_csv( url ); end
105
82
 
106
- ## add more save / put / etc. aliases - why? why not?
107
- ## rename to record_html - why? why not?
108
- def record( url, response,
109
- path: nil,
110
- encoding: 'UTF-8',
111
- format: 'html' )
112
-
113
- body_path = "#{Webcache.root}/#{url_to_path( url, path: path )}"
114
- meta_path = "#{body_path}.meta.txt"
115
-
116
- ## make sure path exits
117
- FileUtils.mkdir_p( File.dirname( body_path ) )
118
-
119
-
120
- puts "[cache] saving #{body_path}..."
121
-
122
- ## todo/check: verify content-type - why? why not?
123
- ## note - for now respone.text always assume (converted) to utf8!!!!!!!!!
124
- ##
125
- ## fix: newlines - always use "unix" style" - why? why not?
126
- ## fix: use :newline => :universal option? translates to univeral "\n"
127
- if format == 'json'
128
- File.open( body_path, 'w:utf-8' ) {|f| f.write( JSON.pretty_generate( response.json )) }
129
- elsif format == 'csv'
130
- ## fix: newlines - always use "unix" style" - why? why not?
131
- ## fix: use :newline => :universal option? translates to univeral "\n"
132
- text = response.text( encoding: encoding ).gsub( "\r\n", "\n" )
133
- File.open( body_path, 'w:utf-8' ) {|f| f.write( text ) }
134
- else ## html or txt
135
- text = response.text( encoding: encoding ).gsub( "\r\n", "\n" )
136
- File.open( body_path, 'w:utf-8' ) {|f| f.write( text ) }
137
- end
138
83
 
84
+ #### new - read (cached) meta data
85
+ ## todo/check - find a better/different name - why? why not?
86
+ ## e.g. read_headers or simply meta or headers or such
87
+ def self.read_meta( url ) cache.read_meta( url ); end
139
88
 
140
- File.open( meta_path, 'w:utf-8' ) do |f|
141
- ## todo/check:
142
- ## do headers also need to converted (like text) if encoding is NOT utf-8 ???
143
- response.headers.each do |key, value| # iterate all response headers
144
- f.write( "#{key}: #{value}" )
145
- f.write( "\n" )
146
- end
147
- end
89
+ ## add convenience expire (shortcut) helpers
90
+ def self.expired?( url, expires_in: Time.now.utc-60*60*12 )
91
+ if cached?( url )
92
+ meta = read_meta( url )
93
+ meta.expired?( expires_in )
94
+ else
95
+ true # note - not in cache; expired by default
96
+ end
148
97
  end
98
+ def self.expired_in_12h?( url ) expired?( url, expires_in: Time.now.utc-60*60*12 ); end
99
+ def self.expired_in_24h?( url ) expired?( url, expires_in: Time.now.utc-60*60*24 ); end
149
100
 
150
101
 
151
-
152
- ### note: use file path as id for DiskCache (is different for DbCache/SqlCache?)
153
- ## use file:// instead of disk:// - why? why not?
154
- def url_to_id( str ) "disk://#{url_to_path( str )}"; end
155
-
156
-
157
- ### helpers
158
- def url_to_path( str, path: nil )
159
- ## map url to file path
160
- uri = URI.parse( str )
161
-
162
- ## note: ignore scheme (e.g. http/https)
163
- ## and post (e.g. 80, 8080, etc.) for now
164
- ## always downcase for now (internet domain is case insensitive)
165
- host_dir = uri.host.downcase
166
-
167
- req_path = if path ## use "custom" (file)path for cache storage if passed in
168
- path
169
- else
170
- ## "/this/is/everything?query=params"
171
- ## cut-off leading slash and
172
- ## convert query ? =
173
- uri.request_uri[1..-1]
174
- end
175
-
176
-
177
-
178
- ### special "prettify" rule for weltfussball
179
- ## /eng-league-one-2019-2020/ => /eng-league-one-2019-2020.html
180
- if host_dir.index( 'weltfussball.de' ) ||
181
- host_dir.index( 'worldfootball.net' )
182
- if req_path.end_with?( '/' )
183
- req_path = "#{req_path[0..-2]}.html"
184
- else
185
- puts "ERROR: expected request_uri for >#{host_dir}< ending with '/'; got: >#{req_path}<"
186
- exit 1
187
- end
188
- elsif host_dir.index( 'tipp3.at' )
189
- req_path = req_path.sub( '.jsp', '' ) # shorten - cut off .jsp extension
190
-
191
- ## change ? to -I-
192
- ## change = to ~
193
- ## Example:
194
- ## sportwetten/classicresults.jsp?oddsetProgramID=888
195
- ## =>
196
- ## sportwetten/classicresults-I-oddsetProgramID~888
197
- req_path = req_path.gsub( '?', '-I-' )
198
- .gsub( '=', '~')
199
-
200
- req_path = "#{req_path}.html"
201
- elsif host_dir.index( 'fbref.com' )
202
- req_path = req_path.sub( 'en/', '' ) # shorten - cut off en/
203
- req_path = "#{req_path}.html" # auto-add html extension
204
- elsif host_dir.index( 'football-data.co.uk' )
205
- req_path = req_path.sub( 'mmz4281/', '' ) # shorten - cut off mmz4281/
206
- req_path = req_path.sub( 'new/', '' ) # shorten - cut off new/
207
- elsif host_dir.index( 'football-data.org' )
208
- ## req_path = req_path.sub( 'v2/', '' ) # shorten - cut off v2/
209
-
210
- ## flattern - make a file path - for auto-save
211
- ## change ? to -I-
212
- ## change / to ~~
213
- ## change = to ~
214
- req_path = req_path.gsub( '?', '-I-' )
215
- .gsub( '/', '~~' )
216
- .gsub( '=', '~')
217
-
218
- req_path = "#{req_path}.json"
219
- elsif host_dir.index( 'api.cryptokitties.co' )
220
- ## for now always auto-add .json extensions e.g.
221
- ## kitties/1 => kitties/1.json
222
- ## cattributes => cattributes.json
223
- req_path = "#{req_path}.json"
224
- else
225
- ## no special rule
226
- end
227
-
228
- page_path = "#{host_dir}/#{req_path}"
229
- page_path
102
+ class << self
103
+ alias_method :exist?, :cached?
104
+ alias_method :expired_in_1d?, :expired_in_24h?
230
105
  end
231
- end # class DiskCache
232
-
233
-
234
106
  end # module Webcache
@@ -0,0 +1,86 @@
1
+
2
+ module Webcache
3
+ class DiskCache
4
+
5
+ ####
6
+ ## todo/fix - make rewrite_path configurable "pipeline"
7
+ ## lets you auto-add more rewriters
8
+ ## and keep this code "generic"
9
+ ## move rewriters "downstream" into "userland" ??
10
+
11
+ def rewrite_path( host, req_path )
12
+
13
+ ### special "prettify" rule for weltfussball
14
+ ## /eng-league-one-2019-2020/ => /eng-league-one-2019-2020.html
15
+
16
+ ### todo/fix - move rules downstream to user - why? why not?
17
+
18
+ if host.include?( 'uefa.com' ) ||
19
+ host.include?( 'kicker.de' ) ||
20
+ host.include?( 'kicker.at' )
21
+ if req_path.end_with?( '/' )
22
+ req_path = "#{req_path[0..-2]}.html"
23
+ else
24
+ puts "ERROR: expected request_uri for >#{host}< ending with '/'; got: >#{req_path}<"
25
+ exit 1
26
+ end
27
+ elsif host.include?( 'weltfussball.de' ) ||
28
+ host.include?( 'worldfootball.net' )
29
+ if req_path.end_with?( '/' )
30
+ req_path = "#{req_path[0..-2]}.html"
31
+ else
32
+ puts "ERROR: expected request_uri for >#{host}< ending with '/'; got: >#{req_path}<"
33
+ exit 1
34
+ end
35
+ elsif host.include?( 'tipp3.at' )
36
+ req_path = req_path.sub( '.jsp', '' ) # shorten - cut off .jsp extension
37
+
38
+ ## change ? to -I-
39
+ ## change = to ~
40
+ ## Example:
41
+ ## sportwetten/classicresults.jsp?oddsetProgramID=888
42
+ ## =>
43
+ ## sportwetten/classicresults-I-oddsetProgramID~888
44
+ req_path = req_path.gsub( '?', '-I-' )
45
+ .gsub( '=', '~')
46
+
47
+ req_path = "#{req_path}.html"
48
+ elsif host.include?( 'fbref.com' )
49
+ req_path = req_path.sub( 'en/', '' ) # shorten - cut off en/
50
+ req_path = "#{req_path}.html" # auto-add html extension
51
+ elsif host.include?( 'football-data.co.uk' )
52
+ req_path = req_path.sub( 'mmz4281/', '' ) # shorten - cut off mmz4281/
53
+ req_path = req_path.sub( 'new/', '' ) # shorten - cut off new/
54
+ elsif host.include?( 'football-data.org' )
55
+ ## req_path = req_path.sub( 'v2/', '' ) # shorten - cut off v2/
56
+
57
+ ## flattern - make a file path - for auto-save
58
+ ## change ? to -I-
59
+ ## change / to ~~
60
+ ## change = to ~
61
+ req_path = req_path.gsub( '?', '-I-' )
62
+ .gsub( '/', '~~' )
63
+ .gsub( '=', '~')
64
+
65
+ req_path = "#{req_path}.json"
66
+ elsif host.include?( 'api-sports.io' )
67
+ req_path = req_path.gsub( '?', '-I-' )
68
+ .gsub( '&', '~~' ) ### check if & present?
69
+ .gsub( '=', '~')
70
+
71
+ req_path = "#{req_path}.json"
72
+ elsif host.include?( 'api.cryptokitties.co' )
73
+ ## for now always auto-add .json extensions e.g.
74
+ ## kitties/1 => kitties/1.json
75
+ ## cattributes => cattributes.json
76
+ req_path = "#{req_path}.json"
77
+ else
78
+ ## no special rule
79
+ end
80
+
81
+ req_path
82
+ end # method rewrite_path
83
+
84
+
85
+ end # class DiskCache
86
+ end # module Webcache
@@ -0,0 +1,244 @@
1
+
2
+ module Webcache
3
+
4
+
5
+ class DiskCache ### todo/check - change to Disk - why? why not?
6
+
7
+
8
+ def _body_path( url ) "#{Webcache.root}/#{url_to_path( url )}"; end
9
+ def _meta_path( url ) "#{Webcache.root}/#{url_to_path( url )}.meta.txt"; end
10
+
11
+
12
+ def cached?( url )
13
+ body_path = _body_path( url )
14
+ exist = File.exist?( body_path )
15
+
16
+ =begin
17
+ ## not really working - check back later
18
+ ### The catch on Windows
19
+ ## On Windows, File.realpath does NOT normalize casing to the on-disk canonical case.
20
+ ##
21
+ ## note - on windows - file.exist? is case-insensitive
22
+ ## use the strict: true flag if you want to enforce case-sensitive exists checks on windows!!!
23
+ ## On Windows, File.realpath returns the "true" path
24
+ ## as stored on the disk with the correct casing.
25
+ ## If the path you provide doesn't match the casing of the real path,
26
+ ## you know the match was case-insensitive.
27
+ if exist && strict
28
+ exist = File.realpath(body_path) == File.expand_path(body_path)
29
+ end
30
+ =end
31
+
32
+ exist
33
+ end
34
+ alias_method :exist?, :cached?
35
+
36
+
37
+
38
+ ### fix-fix-fix
39
+ ## change to read_txt/read_text/read_html
40
+ ## plus add
41
+ ## read_blob/read_bin(ary) !!!
42
+ def read( url )
43
+ _read_utf8(_body_path( url ))
44
+ end
45
+
46
+
47
+
48
+ def read_json( url )
49
+ txt = _read_utf8(_body_path( url ))
50
+ data = JSON.parse( txt )
51
+ data
52
+ end
53
+
54
+ def read_csv( url )
55
+ txt = _read_utf8(_body_path( url ))
56
+ data = CsvHash.parse( txt )
57
+ data
58
+ end
59
+
60
+
61
+ def read_meta( url )
62
+ txt = _read_utf8(_meta_path( url ))
63
+ data = Headers.parse( txt )
64
+ data
65
+ end
66
+
67
+
68
+
69
+
70
+ ## add more save / put / etc. aliases - why? why not?
71
+ ## rename to record_html - why? why not?
72
+
73
+ def record( url, response, format: )
74
+
75
+ ###
76
+ ## note - encoding_user MUST be passed along with response (wrapper) obj
77
+ ## e.g. response._encoding_user = encoding ??
78
+ ## see Webget.page|text|dataset|etc.
79
+
80
+
81
+ ## todo/check - use rel_path or local_path or such??
82
+ save_path = url_to_path( url )
83
+
84
+ body_path = _body_path( url )
85
+ meta_path = _meta_path( url ) ## is _body_path + ".meta.txt"
86
+
87
+ ## make sure path exits
88
+ FileUtils.mkdir_p( File.dirname( body_path ) )
89
+
90
+
91
+ puts "[cache] saving #{body_path}..."
92
+
93
+ ## todo/check: verify content-type - why? why not?
94
+ ## note - for now respone.text always assume (converted) to utf8!!!!!!!!!
95
+
96
+ if format == 'json'
97
+ _write_utf8( body_path, JSON.pretty_generate( response.json ))
98
+ x_encoding = nil ## for now do not track; always assume UTF-8
99
+ x_encoding_source = nil
100
+ x_encoding_valid = nil
101
+ x_ascii_only = nil
102
+ x_8bit = nil
103
+ x_utf8_replace = nil
104
+ else ## html, txt or csv
105
+ _write_utf8( body_path, response.text )
106
+
107
+ x_encoding = response._text_encoding
108
+ x_encoding_source = response._text_encoding_source
109
+ x_encoding_valid = response._text_encoding_valid # true|false or nil (undef)
110
+ x_ascii_only = response._text_ascii_only
111
+ x_8bit = response._text_8bit
112
+ x_utf8_replace = response._text_utf8_replace
113
+ end
114
+
115
+
116
+ ### fix-fix-fix -- add support for binary/image formats
117
+ ## e.g. bin|gif|jpg|etc - why? why not?
118
+
119
+ ####
120
+ ## get file size in bytes
121
+ ## or use File.stat( body_path ).size (using File::Stat) ??
122
+ x_size = File.size( body_path )
123
+
124
+
125
+
126
+ ## todo/check:
127
+ ## do headers also need to converted (like text) if encoding is NOT utf-8 ???
128
+
129
+
130
+ #### add our own custom headers first!!
131
+ ## change x-save to x-filename or ??
132
+ ## change to x-7bit-only or x-ascii7bit or x-ascii7bit-only or ??
133
+ ## change x-size to x-bytesize or ??
134
+ ## change x-8bit to ???
135
+
136
+ ### start w/ comment line
137
+ ### uncomment - http status - why? why not?
138
+ buf = String.new
139
+ buf << "# fetched on #{Time.now.utc}\n"
140
+ buf << "# HTTP/#{response.version} #{response.status.code} #{response.status.message}\n"
141
+ buf << "\n"
142
+
143
+ buf << "x-url: #{url}\n"
144
+ buf << "x-encoding: #{x_encoding}\n" if x_encoding
145
+ buf << "x-encoding-source: #{x_encoding_source}\n" if x_encoding_source
146
+ buf << "x-encoding-valid: #{x_encoding_valid}\n" if x_encoding_valid
147
+ buf << "x-ascii-only: #{x_ascii_only}\n" if x_ascii_only
148
+ buf << "x-8bit: #{x_8bit}\n" if x_8bit
149
+ buf << "x-utf8-replace: #{x_utf8_replace}\n" if x_utf8_replace
150
+ buf << "x-save: #{save_path}\n"
151
+ buf << "x-size: #{x_size}\n"
152
+ buf << "x-format: #{format}\n" ## e.g. json|html|csv|etc.
153
+ buf << "\n"
154
+
155
+ # iterate all response headers
156
+ response.headers.each do |key, value|
157
+ buf << "#{key}: #{value}\n"
158
+ end
159
+
160
+ _write_utf8( meta_path, buf )
161
+ end
162
+
163
+
164
+
165
+ ### note: use file path as id for DiskCache (is different for DbCache/SqlCache?)
166
+ ## use file:// instead of disk:// - why? why not?
167
+ ## def url_to_id( str ) "disk://#{url_to_path( str )}"; end
168
+
169
+
170
+ ### helpers
171
+
172
+ def url_to_path( str )
173
+ ## map url to file path
174
+ uri = URI( str ) ## URI() same as URI.parse()
175
+
176
+ ## note: ignore scheme (e.g. http/https)
177
+ ## and post (e.g. 80, 8080, etc.) for now
178
+ ## always downcase for now (internet domain is case insensitive)
179
+ host_dir = uri.host.downcase
180
+
181
+ ## "/this/is/everything?query=params"
182
+ ## cut-off leading slash and
183
+ ## convert query ? =
184
+ ## check if [1..] is sames as [1..-1]
185
+ req_path = rewrite_path( host_dir, uri.request_uri[1..] )
186
+
187
+
188
+ page_path = "#{host_dir}/#{req_path}"
189
+ page_path
190
+ end
191
+
192
+
193
+
194
+ def _read_utf8( path )
195
+ ## note - by default ruby on windows (automagically)
196
+ ## translates \r\n newlines to \n (universal/unix-style)
197
+ ## when read files!!!!
198
+ ## note - only handles \r\n (not "legacy" mac classic-style \r)
199
+
200
+ File.open( path, 'r:utf-8' ) {|f| f.read }
201
+ end
202
+
203
+ def _write_utf8( path, text )
204
+ ## write out utf8 (always use "universal" newlines on any platform)
205
+ ## todo / fix - add universial or such to open too ?
206
+ ##
207
+ ## fix: newlines - always use "unix" style" - why? why not?
208
+ ## fix: use :newline => :universal option? translates to univeral "\n"
209
+ ##
210
+ ##
211
+ ## The universal_newline: true flag forces Ruby to look through the string and
212
+ ## convert both Windows-style (\r\n) and old Mac-style (\r) newlines
213
+ ## into the standard Unix newline (\n)
214
+ ##
215
+ ## File.open("output.txt", "w", universal_newline: true) do |file|
216
+ ## file.write(content)
217
+ ## end
218
+ ##
219
+ ## mixed_string = "Line one\r\nLine two\rLine three\n"
220
+ ##
221
+ ## Converts all \r\n and \r into \n
222
+ ## clean_string = mixed_string.encode(universal_newline: true)
223
+ ##
224
+ ## Why Use encode Instead of gsub?
225
+ ## While many developers use regular expressions like .gsub(/\r\n?/, "\n"),
226
+ ## using .encode is highly preferred because:
227
+ ## Edge-case Safety: It is an internal, optimized C-level implementation
228
+ ## that handles mixed and broken newline edges perfectly.
229
+ ## Encoding Preservation: It seamlessly preserves the existing character encoding
230
+ ## (e.g., UTF-8) of your string.
231
+ ##
232
+ ## was - text = text.gsub( "\r\n", "\n" )
233
+ ##
234
+ ## note - by default ruby on windows (automagically) translates newlines to \r\n (crlf)!!!
235
+ ## thus, always use/ add universal_newline flag!!!
236
+
237
+ File.open( path, 'w:utf-8', universal_newline: true ) do |f|
238
+ f.write( text )
239
+ end
240
+ end
241
+ end # class DiskCache
242
+
243
+
244
+ end ## module Webcache
@@ -0,0 +1,79 @@
1
+
2
+ module Webcache
3
+
4
+ ##############################
5
+ # nested class for convenience access to (meta) headers
6
+
7
+
8
+ class Headers ## todo/check - rename to Meta or such - why? why not?
9
+
10
+
11
+ ## def self.read( path ) parse(read_text( path )); end
12
+
13
+
14
+
15
+ def self.parse( txt )
16
+ data = {}
17
+ txt.each_line do |line|
18
+ line = line.strip
19
+ next if line.empty? || line.start_with?( '#' )
20
+
21
+ key, value = line.split( ':', 2 ) ## split on first colon
22
+
23
+ ## todo/fix: deal with possible duplicate header keys!!
24
+ ## if duplicate do NOT replease, add with leading ", " comma-separated!!!
25
+ ##
26
+ ## check if multi-line headers are possible!!!
27
+ ## and than may turn value into array of values - why? why not?
28
+ ## or auto-add
29
+
30
+
31
+ ## note - always downcase keys for now
32
+ ## and strip value from leading and trailing spaces
33
+ data[ key.strip.downcase ] = value.strip
34
+ end
35
+ new( data )
36
+ end
37
+
38
+
39
+
40
+ def initialize( data )
41
+ @data = data
42
+ end
43
+
44
+ def to_h() @data; end
45
+ def [](key) @data[key]; end
46
+
47
+ def each( &blk )
48
+ @data.each do |key, value|
49
+ blk.call( key, value )
50
+ end
51
+ end
52
+
53
+
54
+
55
+ def date
56
+ ## return date header
57
+ ## parses the time as RFC 1123 date of HTTP-date defined by RFC 2616:
58
+ ## day-of-week, DD month-name CCYY hh:mm:ss GMT
59
+ ## !!! Note that the result is always UTC (GMT). !!!
60
+ ## e.g. Sun, 19 May 2024 15:15:34 GMT
61
+ ## Mon, 10 Jun 2024 15:58:16 GMT
62
+ @date ||= Time.httpdate( @data['date'] )
63
+ @date
64
+ end
65
+
66
+ ## default to 12h (60secs*60min*12h)
67
+ def expired?( expires_in_date=Time.now.utc-60*60*12 )
68
+ ## pp expires_in_date
69
+ expires_in_date > date
70
+ end
71
+
72
+ ## add convenience helpers - why? why not?
73
+ def expired_in_12h?() expired?( Time.now.utc-60*60*12 ); end
74
+ def expired_in_24h?() expired?( Time.now.utc-60*60*24 ); end
75
+ alias_method :expired_in_1d?, :expired_in_24h?
76
+ end # class Headers
77
+
78
+
79
+ end ## module Webcache
data/lib/webget/webget.rb CHANGED
@@ -22,16 +22,15 @@ class Webget # a web (go get) crawler
22
22
 
23
23
 
24
24
 
25
- ## note - assumes json format
25
+ ## note - assumes json format
26
26
  ## encoding always utf-8 by definition! - double check?)
27
- def self.call( url, headers: {} )
27
+ def self.call( url, headers: {} )
28
28
  response = _get( url, headers: headers )
29
29
 
30
30
  if response.status.ok? ## must be HTTP 200
31
31
  puts "#{response.status.code} #{response.status.message}"
32
32
  ## note: use format json for pretty printing and parse check!!!!
33
- Webcache.record( url, response,
34
- format: 'json' )
33
+ Webcache.record( url, response, format: 'json' )
35
34
  else
36
35
  ## todo/check - log error
37
36
  puts "!! HTTP ERROR - #{response.status.code} #{response.status.message}:"
@@ -40,17 +39,22 @@ class Webget # a web (go get) crawler
40
39
 
41
40
  ## to be done / continued
42
41
  response
43
- end # method self.call
42
+ end # method self._get_and_record
43
+
44
+
45
+
44
46
 
45
47
  ## todo/check: rename encoding to html/http-like charset - why? why not?
46
48
  ## check encoding UTF-8 or utf-8 - makes a difference?
47
- def self.page( url, encoding: 'UTF-8', headers: {} ) ## assumes html format
49
+ ## note - default text encoding "upstream" in webclient for text is UTF-8!!
50
+
51
+ def self.page( url, encoding: nil, headers: {} ) ## assumes html format
48
52
  response = _get( url, headers: headers )
53
+ response._encoding_user = encoding
49
54
 
50
55
  if response.status.ok? ## must be HTTP 200
51
56
  puts "#{response.status.code} #{response.status.message}"
52
- Webcache.record( url, response,
53
- encoding: encoding ) ## assumes format: html (default)
57
+ Webcache.record( url, response, format: 'html' )
54
58
  else
55
59
  ## todo/check - log error
56
60
  puts "!! HTTP ERROR - #{response.status.code} #{response.status.message}:"
@@ -63,15 +67,13 @@ class Webget # a web (go get) crawler
63
67
 
64
68
 
65
69
  ## assumes txt format
66
- def self.text( url, path: nil, headers: {} )
70
+ def self.text( url, encoding: nil, headers: {} )
67
71
  response = _get( url, headers: headers )
72
+ response._encoding_user = encoding
68
73
 
69
74
  if response.status.ok? ## must be HTTP 200
70
75
  puts "#{response.status.code} #{response.status.message}"
71
- ## note: like json assumes always utf-8 encoding for now !!!
72
- Webcache.record( url, response,
73
- path: path, ## optional "custom" (file)path for saving in cache
74
- format: 'txt' )
76
+ Webcache.record( url, response, format: 'txt' )
75
77
  else
76
78
  ## todo/check - log error
77
79
  puts "!! HTTP ERROR - #{response.status.code} #{response.status.message}:"
@@ -86,14 +88,13 @@ class Webget # a web (go get) crawler
86
88
 
87
89
  ## todo/check: rename to csv or file or records or - why? why not?
88
90
  ## todo/check: rename encoding to html/http-like charset - why? why not?
89
- def self.dataset( url, encoding: 'UTF-8', headers: {} ) ## assumes csv format
91
+ def self.dataset( url, encoding: nil, headers: {} ) ## assumes csv format
90
92
  response = _get( url, headers: headers )
93
+ response._encoding_user = encoding
91
94
 
92
95
  if response.status.ok? ## must be HTTP 200
93
96
  puts "#{response.status.code} #{response.status.message}"
94
- Webcache.record( url, response,
95
- encoding: encoding,
96
- format: 'csv' ) ## pass along csv format - why? why not?
97
+ Webcache.record( url, response, format: 'csv' ) ## pass along csv format - why? why not?
97
98
  else
98
99
  ## todo/check - log error
99
100
  puts "!! HTTP ERROR - #{response.status.code} #{response.status.message}:"
@@ -106,8 +107,9 @@ class Webget # a web (go get) crawler
106
107
 
107
108
 
108
109
 
110
+
109
111
  ####
110
- ## private helpers
112
+ ## private helpers
111
113
  ## make private - why? why not?
112
114
  def self._get( url, headers: {} )
113
115
  @@requests ||= 0 ## track number of requests
@@ -121,4 +123,3 @@ class Webget # a web (go get) crawler
121
123
  Webclient.get( url, headers: headers ) ## returns respone
122
124
  end
123
125
  end # class Webget
124
-
data/lib/webget.rb CHANGED
@@ -10,7 +10,13 @@ require 'cocos' # - note - cococs incl. webclient & cvsreader !!!!
10
10
 
11
11
  ## our own code
12
12
  require_relative 'webget/version' # let version go first
13
+
14
+ require_relative 'webget/webcache_headers'
15
+ require_relative 'webget/webcache_disk'
16
+ require_relative 'webget/webcache_disk-rewrite'
13
17
  require_relative 'webget/webcache'
18
+
19
+
14
20
  require_relative 'webget/webget'
15
21
 
16
22
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webget
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gerald Bauer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-04 00:00:00.000000000 Z
11
+ date: 2026-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: webclient
@@ -16,28 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.2.0
19
+ version: 0.3.1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.2.0
27
- - !ruby/object:Gem::Dependency
28
- name: csvreader
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: 1.2.4
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: 1.2.4
26
+ version: 0.3.1
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: cocos
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -78,14 +64,14 @@ dependencies:
78
64
  requirements:
79
65
  - - "~>"
80
66
  - !ruby/object:Gem::Version
81
- version: '4.1'
67
+ version: '4.2'
82
68
  type: :development
83
69
  prerelease: false
84
70
  version_requirements: !ruby/object:Gem::Requirement
85
71
  requirements:
86
72
  - - "~>"
87
73
  - !ruby/object:Gem::Version
88
- version: '4.1'
74
+ version: '4.2'
89
75
  description: webget gem - a web (go get) crawler incl. web cache
90
76
  email: gerald.bauer@gmail.com
91
77
  executables: []
@@ -102,8 +88,11 @@ files:
102
88
  - lib/webget.rb
103
89
  - lib/webget/version.rb
104
90
  - lib/webget/webcache.rb
91
+ - lib/webget/webcache_disk-rewrite.rb
92
+ - lib/webget/webcache_disk.rb
93
+ - lib/webget/webcache_headers.rb
105
94
  - lib/webget/webget.rb
106
- homepage: https://github.com/rubycoco/webclient
95
+ homepage: https://github.com/rubycocos/webclient
107
96
  licenses:
108
97
  - Public Domain
109
98
  metadata: {}
@@ -117,14 +106,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
106
  requirements:
118
107
  - - ">="
119
108
  - !ruby/object:Gem::Version
120
- version: 2.2.2
109
+ version: 3.1.0
121
110
  required_rubygems_version: !ruby/object:Gem::Requirement
122
111
  requirements:
123
112
  - - ">="
124
113
  - !ruby/object:Gem::Version
125
114
  version: '0'
126
115
  requirements: []
127
- rubygems_version: 3.4.10
116
+ rubygems_version: 3.5.22
128
117
  signing_key:
129
118
  specification_version: 4
130
119
  summary: webget gem - a web (go get) crawler incl. web cache