webget-mirror 0.0.1

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.
@@ -0,0 +1,222 @@
1
+
2
+
3
+ class Webget
4
+ class Mirror
5
+
6
+
7
+ ## double assert
8
+ ## assert - double check
9
+ ## make sure url.path does NOT start with // or
10
+ ## /// !!
11
+ ## and does NOT end_with /
12
+ ## pages
13
+
14
+ def _broken_path?( path )
15
+ path.start_with?( '//' ) ||
16
+ path.end_with?( '/' ) ||
17
+ !path.start_with?( '/' ) ## note - MUST start with single slash (/)
18
+ end
19
+
20
+
21
+
22
+ ## get all links
23
+ ## ignore anchor links and
24
+ ## split into internal and external
25
+ def _find_links( site:,
26
+ doc:,
27
+ url:,
28
+ verbose: true )
29
+
30
+
31
+ ## note - base_url is the doc(ument) url
32
+ ## e.g. https://rsssf.org/curtour.html
33
+ base_url = URI( url )
34
+
35
+
36
+ if _broken_path?( base_url.path )
37
+ puts "!! normalized base_url.path expected - got:"
38
+ pp url
39
+ pp base_url
40
+ exit 1
41
+ end
42
+
43
+ ##
44
+ ## note: Array#compact removes all nil values from an array.
45
+ ## if no href in a - nokigiri return nil
46
+ ##
47
+ ## might still incl. empty string ("") - remove too - why? why not?
48
+
49
+ ##
50
+ ## fix - change to css('a[href]') or such ??
51
+ ### document.css("a[href]").each do |a|
52
+ ## links = doc.css('a').map { |a| a['href'] }.compact
53
+
54
+
55
+ links = doc.css( 'a[href]' ).map do |a|
56
+ ## strip leading & trailing spaces e.g.
57
+ ## "http://www.danskfodbold.dk "
58
+ ## is invalid url!!!
59
+ a['href'].strip
60
+ end.reject do |href|
61
+ # skip
62
+ # - empty strings,
63
+ # - page anchors
64
+ href.empty? || href.start_with?('#') ||
65
+
66
+ ## skip mailto links/javascript snippets
67
+ href.match?( /\A(?:mailto|javascript)/i ) ||
68
+
69
+ ## also skip broken mailto links
70
+ ## that is, missing mailto
71
+ ## e.g.
72
+ href.include?( '@' )
73
+ end
74
+
75
+
76
+ ## split into internal & external
77
+ ## make links absolute
78
+ ## ignore anchor links (see above)
79
+
80
+ pages = []
81
+ externals = []
82
+
83
+ links.each do |href|
84
+
85
+ ##
86
+ ## auto-fix ("site-wide") known quirks:
87
+ href = site.autofix_href.call( href ) if site.autofix_href.is_a?( Proc )
88
+
89
+
90
+ page_url = nil
91
+ begin
92
+
93
+ ## special case
94
+ ## check for protocol-relative // e.g. //hello.html
95
+ ## NOT handled by URI
96
+ ## URI makes hello.html into host !!!
97
+ ## host is hello.html and path is nil
98
+ ## only works properly with triple ///
99
+ ## e.g. ///hello.html
100
+ ## now host is nil, and path is /hello.html
101
+
102
+ ## URI.join(URI("https://example.com/page.html"), "//cdn.example.com/file.js")
103
+ ## # => #<URI::HTTPS https://cdn.example.com/file.js> ✓ Works!
104
+ ##
105
+ ## But with just the string:
106
+ ## URI("//cdn.example.com/file.js")
107
+ ## Parses incorrectly—no scheme, treats cdn.example.com as host !!!!!
108
+
109
+
110
+ ## The browser breaks down //path/page.html like this:
111
+ ## - Protocol: Inherited from the current page (e.g., https:).
112
+ ## - Domain (Authority): path
113
+ ## - File Path: /page.html
114
+ ##
115
+ ## If your website is hosted on https://example.com and
116
+ ## a user clicks <a href="//path/page.html">, the browser will try
117
+ ## to navigate to https://path/page.html.
118
+ ## Unless you own a domain name that is literally just path,
119
+ ## this will result in a "Site cannot be reached" error.
120
+ ###
121
+ ### "legacy" protocol relative is "//://" !!!!
122
+ ##
123
+ ## move notes from here to dedicated notes page!!
124
+
125
+
126
+
127
+ if href.start_with?("//")
128
+ puts "!!! debug break on href starting with //:"
129
+ pp href
130
+ pp url
131
+ pp base_url
132
+ exit 1
133
+ end
134
+
135
+
136
+
137
+ ## check if href is absolute?
138
+ href_url = URI( href )
139
+
140
+ ## assume already absolute
141
+ if href_url.scheme && href_url.host
142
+ page_url = href_url
143
+ else
144
+ ## try to make absolute (relative to base_url)
145
+ page_url = URI.join(base_url, href_url)
146
+ end
147
+
148
+ rescue => ex
149
+ ## skip bad urls and log
150
+
151
+ msg = "bad url in #{base_url.path}:\n#{href}\nex:#{ex}\n"
152
+
153
+ ## note - only report in verbose mode (fresh download or such)!!!
154
+ if verbose
155
+ log( msg )
156
+ puts "!! " + msg
157
+ end
158
+
159
+ next
160
+ end
161
+
162
+ ###
163
+ ## fix-fix-fix
164
+ ## check for optional www too
165
+ ## assume same for now ??
166
+ ## or better add to autofix
167
+ ## if www.rsssf.org change to rsssf.org
168
+
169
+ if page_url.host == site.host ## e.g. 'rsssf.org'
170
+ if page_url.path == base_url.path
171
+ puts " anchor #{href} => #{page_url.fragment}" if verbose
172
+ else
173
+ puts " internal page #{href} => #{page_url.path}" if verbose
174
+
175
+ ## note - for internal pages
176
+ ## for now no SUPPORT for query
177
+ ## e.g. foo=1&bar=2
178
+ if page_url.query
179
+ ## change to ValueError or such - why? why not?
180
+ ## raise ArgumentError, "query in internal page links not yet supported, sorry - got #{page_url}"
181
+ msg = "query in internal page links not yet supported, sorry - got #{page_url}"
182
+ puts "!! WARN - #{msg}"
183
+ log( msg )
184
+ next
185
+ end
186
+
187
+ if _broken_path?( page_url.path )
188
+ puts "!! normalized page_url.path expected - got:"
189
+ pp page_url.path
190
+ pp page_url
191
+ puts "base_url:"
192
+ pp url
193
+ pp base_url
194
+ exit 1
195
+ end
196
+
197
+ pages << page_url.path
198
+ end
199
+ else
200
+ puts "!! external #{href} => #{page_url}" if verbose
201
+ externals << page_url.to_s
202
+ end
203
+ end
204
+
205
+ ## make uniq
206
+ pages = pages.uniq
207
+ externals = externals.uniq
208
+
209
+ if verbose
210
+ puts " #{pages.size} internal & #{externals.size} external link(s) found in #{base_url.path}:"
211
+
212
+
213
+ pp pages
214
+ pp externals
215
+ end
216
+
217
+ [pages, externals]
218
+ end
219
+
220
+
221
+ end ## class Mirror
222
+ end ## class Webget
@@ -0,0 +1,246 @@
1
+
2
+ ## use recursive_download_page or such - why? why not?
3
+ ## or add recursive flag
4
+
5
+ =begin
6
+ visited: 100 (downloaded: 98) - 35586 page(s) indexed (9158 cached, 26428 missing)
7
+ [98/26526 - 0.00%] 2:14 mins - 1.38 secs/page, estimate: 608:53 mins
8
+ =end
9
+
10
+
11
+ class Webget
12
+ class Mirror
13
+
14
+ ##
15
+ ## use limit for batch - why? why not?
16
+ ## start of / try a batch of a hundred
17
+ def _mirror_pages( site:,
18
+ force: false,
19
+ batch: 1000 )
20
+
21
+ visited = 0
22
+ downloaded = 0
23
+
24
+ time_start = Time.now
25
+
26
+ loop do
27
+
28
+ ## (i) prioritize main pages (e.g. use start_pages_path)
29
+ ## /curdom.html
30
+ ## /curtour.html
31
+ ## /histdom.html
32
+ ## /intclub.html
33
+ ## /intland.html
34
+ page_recs = MirrorDb::Model::Page.where( cached: false,
35
+ path: site.start_pages_path
36
+ ).limit( batch )
37
+
38
+
39
+ ## (ii) prefer pages (e.g. use boost_pages_path_like)
40
+ ## starting with /tables,/tables[a-z]/
41
+ if page_recs.size == 0 && site.boost_pages_path_like?
42
+ page_recs = MirrorDb::Model::Page.where( cached: false ).
43
+ where( 'path LIKE ?',
44
+ site.boost_pages_path_like ).limit( batch )
45
+ end
46
+
47
+ ## (iii) retry "unconstrained" if nothing found matching (i & ii)
48
+ if page_recs.size == 0
49
+ page_recs = MirrorDb::Model::Page.where( cached: false ).limit( batch )
50
+ end
51
+
52
+
53
+ ### no more pages - done - break out of loop and say goodbye
54
+ break if page_recs.size == 0
55
+
56
+
57
+
58
+
59
+ page_recs.each_with_index do |page_rec,i|
60
+
61
+ ##
62
+ ## fix-fix-fix - change to mime type - why? why not?
63
+ ## allow pages with no extensions!!!
64
+
65
+ ### special case for non .html/.htm pages (e.g. .pdf others too??)
66
+ ## do NOT download / mirror / cache for now
67
+ if page_rec.not_html?
68
+ page_rec.update!( cached: true )
69
+ next
70
+ end
71
+
72
+
73
+ ## note - on download (not if cached)
74
+ ## encoding
75
+ ## might be get changed
76
+ ## ALWAYS use updated encoding!!
77
+
78
+ ##
79
+ ## note - workaround for windows
80
+ ## on windows File.exist? (and Webcache.cached?)
81
+ ## is case-insensitive
82
+ ## e.g. /USAdave/ is the same as /usadave/
83
+ ##
84
+ ## as a workaround ALWAYS hardcode 404
85
+ ## for /USAdave/ to get (and record) 404 (and not CACHE HITS!!)
86
+ ## e.g. try https://rsssf.org/USAdave/cncc.html => 404 (NOT FOUND)
87
+ ## https://rsssf.org/usadave/cncc.html => 200 (OK)
88
+
89
+
90
+ ## note - url e.g. https://rsssf.org
91
+ ## path MUST start with / e.g. /curtour.html
92
+ ## resulting in https://rsssf.org/curtour.html
93
+
94
+ url = site.base_url+page_rec.path
95
+
96
+ ## if %r{/USAdave/}.match?(page_rec.path)
97
+ ## ['', {status: 404}]
98
+
99
+
100
+
101
+ ## check if not in cache
102
+ ## note - use force == true to always (force) download
103
+
104
+ html, response_meta = _download_page( url,
105
+ encoding: page_rec.encoding,
106
+ force: force )
107
+
108
+ if response_meta
109
+ downloaded += 1
110
+ puts " --- " + fmt_time_diff( time_start, count: downloaded )
111
+
112
+ ###
113
+ ## special case
114
+ ## check for 404 NOT FOUND
115
+ if response_meta[:http_status] == 404
116
+ page_rec.update!( http_status: 404,
117
+ cached: true )
118
+
119
+ next ### note - skip further processing on 404 (no links etc.)!!
120
+ end
121
+ end
122
+
123
+
124
+
125
+ html = site.errata( html, url: url ) if site.errata?
126
+
127
+
128
+
129
+
130
+ ## Standard HTML4-style parsing (default)
131
+ ## doc = Nokogiri::HTML(malformed_html)
132
+ ## -or-
133
+ ## More robust HTML5 parsing
134
+ ##doc = Nokogiri::HTML5(malformed_html)
135
+
136
+ doc = Nokogiri::HTML( html )
137
+
138
+
139
+ ## get (page meta info)
140
+ ## title, tabs (count), html_doctype, html_charset
141
+ page_info = _collect_page_info( doc, html: html )
142
+
143
+
144
+
145
+ ## note - if response meta data present than fresh download (not cached)
146
+ ## cached = response_meta ? false : true
147
+
148
+ ## turn on verbose mode only if page downloaded (not on cache hit)
149
+ verbose = response_meta ? true : false
150
+ ## verbose = true
151
+
152
+ internals, _ = _find_links( site: site,
153
+ doc: doc,
154
+ url: url,
155
+ verbose: verbose
156
+ )
157
+
158
+
159
+ ## add links to db
160
+ internals.each do |path|
161
+ internal_rec = MirrorDb::Model::Page.find_or_create_by!(
162
+ path: path ) do |rec|
163
+ puts " add linked page #{rec.path}"
164
+
165
+ rec.encoding = site.page_encoding( rec.path )
166
+ rec.cached = false
167
+ end
168
+
169
+ ## puts " add link from #{page_rec.path} to #{internal_rec.path} to mirror.db"
170
+ ### note allow - find (may happen after "crash" or interrupt)
171
+ link_rec = MirrorDb::Model::Link.find_or_create_by!(
172
+ from_page_id: page_rec.id,
173
+ to_page_id: internal_rec.id )
174
+ end
175
+
176
+ puts " [#{i+1}/#{page_recs.size}] update page #{page_rec.path} w/ #{internals.size} page(s) linked - >#{page_info[:title] || 'n/a'}<"
177
+
178
+
179
+ ###
180
+ ## note - remove cached (flag) and replace with http_status => nil|200|404|etc?
181
+ ## that is, cached = false => nil
182
+ ## cached = true => 200|404|etc - why? why not??
183
+ attribs = {
184
+ cached: true
185
+ }
186
+ ## add (optional) page_info attribus
187
+ more_attribs = {
188
+ title: page_info[:title], ## note - might be missing (nil) in some pages
189
+ html_doctype: page_info[:html_doctype],
190
+ html_charset: page_info[:html_charset],
191
+ tabs: page_info[:tabs]
192
+ }
193
+ attribs = attribs.merge( more_attribs )
194
+
195
+
196
+ ## check for encoding when fresh download (via response meta data)
197
+ if response_meta
198
+ more_attribs = {
199
+ encoding: response_meta[:encoding] ? response_meta[:encoding].downcase : nil,
200
+ encoding_source: response_meta[:encoding_source], ## bom|html|http|user|fallback
201
+ encoding_valid: response_meta[:encoding_valid], ## nil|true|false
202
+
203
+ ascii7bit: response_meta[:ascii7bit], ## nil|true|false
204
+ ## note - convert 10 - 212=>8, 233=>2 (use only first total count; no details)
205
+ chars_8bit: response_meta[:chars_8bit] ? response_meta[:chars_8bit].to_i(10) : nil,
206
+ utf8_replace: response_meta[:utf8_replace],
207
+
208
+ http_content_type: response_meta[:http_content_type],
209
+ http_content_length: response_meta[:http_content_length],
210
+ http_status: response_meta[:http_status]
211
+ }
212
+ attribs = attribs.merge( more_attribs )
213
+ end
214
+
215
+
216
+ page_rec.update!( **attribs )
217
+
218
+
219
+
220
+ visited += 1
221
+
222
+ if visited % 100 == 0
223
+ puts "\n visited: #{visited} (downloaded: #{downloaded}) - " +
224
+ " #{MirrorDb::Model::Page.count} page(s) indexed " +
225
+ "(#{MirrorDb::Model::Page.cached.count} cached, " +
226
+ "#{MirrorDb::Model::Page.not_cached.count} missing)"
227
+
228
+ puts " " + fmt_time_diff( time_start, step: downloaded,
229
+ count: downloaded+MirrorDb::Model::Page.not_cached.count )
230
+
231
+
232
+ end
233
+ end
234
+ end
235
+
236
+
237
+
238
+ puts "\n visited: #{visited} (downloaded: #{downloaded}) - " +
239
+ " #{MirrorDb::Model::Page.count} page(s) indexed " +
240
+ "(#{MirrorDb::Model::Page.cached.count} cached, " +
241
+ "#{MirrorDb::Model::Page.not_cached.count} missing)"
242
+ end
243
+
244
+
245
+ end ## class Mirror
246
+ end ## class Webget
@@ -0,0 +1,32 @@
1
+
2
+
3
+ class Webget
4
+ class Mirror
5
+
6
+
7
+
8
+ def fmt_time_diff( time_start, time_end=Time.now, count:, step: nil )
9
+ time_diff = time_end - time_start
10
+ buf = String.new
11
+
12
+ if count == 0 || step == 0
13
+ buf += " %d:%02d mins" % [time_diff/60, time_diff%60]
14
+ elsif step
15
+ buf += " [#{step}/#{count} - %5.2f%%]" % [step*100/count]
16
+
17
+ buf += " %d:%02d mins" % [time_diff/60, time_diff%60]
18
+ buf += " - %5.2f secs/page" % [time_diff/step]
19
+
20
+ time_estimate = (time_diff/step) * count
21
+ buf += ", estimate: %d:%02d mins" % [time_estimate/60, time_estimate%60]
22
+ else
23
+ buf += " %d:%02d mins" % [time_diff/60, time_diff%60]
24
+ buf += " - %5.2f secs/page (#{count} pages)" % [time_diff/count]
25
+ end
26
+
27
+ buf
28
+ end
29
+
30
+
31
+ end ## class Mirror
32
+ end ## class Webget
@@ -0,0 +1,22 @@
1
+
2
+ class Webget
3
+ class Mirror
4
+ MAJOR = 0 ## todo: namespace inside version or something - why? why not??
5
+ MINOR = 0
6
+ PATCH = 1
7
+ VERSION = [MAJOR,MINOR,PATCH].join('.')
8
+
9
+ def self.version
10
+ VERSION
11
+ end
12
+
13
+ # version string for generator meta tag (includes ruby version)
14
+ def self.banner
15
+ "webget-mirror/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] in (#{root})"
16
+ end
17
+
18
+ def self.root
19
+ File.expand_path( File.dirname(File.dirname(File.dirname(File.dirname(__FILE__)))) )
20
+ end
21
+ end # class Mirror
22
+ end # class Webget