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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 22d6bef3214b26fff9ccc700e43479887b29888529dd51b33ad67b2506e9c1ed
4
+ data.tar.gz: 65bf372aaacb2cf77e0743d7315a3ad6c82b54827c0c6035b56d1851b8ee8458
5
+ SHA512:
6
+ metadata.gz: 99fc9d25ab2b876130498eccc8d97482fe144e156d037fc37e6b6a7811b5a62c58a735307e6c8341466d67c21a2b3bc09307f7e41a0a8416eb8c7fc35ff3b471
7
+ data.tar.gz: d1addaa18484d4e8b13e19b606f9119a568ff44e1f2a3a8a36431e3153de33c161aaa84f9b226db16d00c07659c6d2164af70c49c19ac187700c916d835dc42d
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ### 0.0.1 / 2026-09-09
2
+
3
+ * Everything is new. First release.
data/Manifest.txt ADDED
@@ -0,0 +1,16 @@
1
+ CHANGELOG.md
2
+ Manifest.txt
3
+ README.md
4
+ Rakefile
5
+ lib/webget-mirror.rb
6
+ lib/webget/mirror.rb
7
+ lib/webget/mirror/collect_page_info.rb
8
+ lib/webget/mirror/database/models.rb
9
+ lib/webget/mirror/database/open.rb
10
+ lib/webget/mirror/database/schema.rb
11
+ lib/webget/mirror/download_page.rb
12
+ lib/webget/mirror/find_links.rb
13
+ lib/webget/mirror/mirror.rb
14
+ lib/webget/mirror/utils.rb
15
+ lib/webget/mirror/version.rb
16
+ lib/webget/mirror/website.rb
data/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # webget-mirror
2
+
3
+ webget-mirror gem - addon to webget to mirror websites
4
+
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
+ * gem :: [rubygems.org/gems/webget-mirror](https://rubygems.org/gems/webget-mirror)
8
+ * rdoc :: [rubydoc.info/gems/webget-mirror](http://rubydoc.info/gems/webget-mirror)
9
+
10
+
11
+ ## Usage
12
+
13
+ TBD
14
+
15
+
16
+ ## License
17
+
18
+ The `webget-mirror` scripts are dedicated to the public domain.
19
+ Use as you please with no restrictions whatsoever.
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ require 'hoe'
2
+ require './lib/webget/mirror/version.rb'
3
+
4
+ Hoe.spec 'webget-mirror' do
5
+
6
+ self.version = Webget::Mirror::VERSION
7
+
8
+ self.summary = 'webget-mirror gem - addon to webget to mirror websites'
9
+ self.description = summary
10
+
11
+ self.urls = { home: 'https://github.com/rubycocos/webclient' }
12
+
13
+ self.author = 'Gerald Bauer'
14
+ self.email = 'gerald.bauer@gmail.com'
15
+
16
+ # switch extension to .markdown for gihub formatting
17
+ self.readme_file = 'README.md'
18
+ self.history_file = 'CHANGELOG.md'
19
+
20
+ self.extra_deps = [
21
+ ['webget', ],
22
+ ['nokogiri'],
23
+ ['activerecord'],
24
+ ['sqlite3'],
25
+ ]
26
+
27
+
28
+ self.licenses = ['Public Domain']
29
+
30
+ self.spec_extras = {
31
+ required_ruby_version: '>= 3.1.0'
32
+ }
33
+
34
+ end
@@ -0,0 +1,107 @@
1
+
2
+
3
+ class Webget
4
+ class Mirror
5
+
6
+
7
+ ###
8
+ # fix-fix-fix
9
+ # make regex more "generic"
10
+ #
11
+ # add real-world samples here
12
+
13
+ =begin
14
+
15
+ rsssf.org/tableso/oost2014.html:
16
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-2">
17
+ rsssf.org/tablesn/nz-intres.html:
18
+ <meta http-equiv="content-type" content="text/html; charset=UTF-16LE">
19
+ rsssf.org/tablesa/argchamp.html:
20
+ <meta http-equiv="Content-Type" content="text/html; charset=UTFs-8">
21
+ => typo - UTFs-8 !!!
22
+ rsssf.org/tables/2002full.html:
23
+ <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
24
+ rsssf.org/miscellaneous/zwed-coach-triv.html:
25
+ <meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
26
+ rsssf.org/tablesr/roem68.html:
27
+ <META http-equiv=Content-Type content="text/html; charset=windows-1250">
28
+
29
+ =end
30
+
31
+
32
+
33
+ HTML_CHARSET_RE = %r{
34
+ <meta [ ]+
35
+ [^<>]*? ## note - use non-greedy (shortest) match
36
+ \bcharset
37
+ [ ]*=[ ]*
38
+ ['"]? ## optional opening quote
39
+ (?<charset>[a-z0-9_-]+)
40
+ }ix
41
+
42
+
43
+ HTML_DOCTYPE_RE = %r{
44
+ <!DOCTYPE [ ]+
45
+ (?<doctype> [^<>]+?) ## note - use non-greedy (shortest) match
46
+ ## do NOT allow opening/closing brackets for now
47
+ ## ever possible? double check
48
+ [ ]*
49
+ >
50
+ }ix
51
+
52
+
53
+
54
+ def _collect_page_info( doc, html: )
55
+
56
+ ## use collect_page_stat( doc: )
57
+ ## or collect_page_info ( pass in nokogiri doc !!)
58
+ ## PageInfo (or Page::Info), PageStat
59
+ ## - title
60
+ ## - html_doctype
61
+ ## - html_charset
62
+ ## - tabs
63
+ ## ...
64
+
65
+ ### try to find page title
66
+ ## not - title might be missing (nil)!!
67
+ title_el = doc.at_css('title')
68
+ title = title_el ? title_el.text.strip : nil
69
+
70
+ ##
71
+ ## note - use "plain-old" regex
72
+ ## to get "raw" doctype/charset from html source
73
+ ##
74
+ ## record doctype
75
+ ## e.g
76
+ ## <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
77
+ ## <!DOCTYPE HTML>
78
+ ## and html charset (inside meta)
79
+ ## e.g.
80
+ ## <meta http-equiv="Content-Type" content="text/html; charset=Windows-1252">
81
+ ## <meta charset="Windows-1252">
82
+
83
+ ###
84
+ ## note - limit search to 1024 ( or allow double 2048)
85
+
86
+ html_doctype = (m=HTML_DOCTYPE_RE.match( html[0,1024] )) ? m[:doctype] : nil
87
+ html_charset = (m=HTML_CHARSET_RE.match( html[0,1024] )) ? m[:charset] : nil
88
+
89
+
90
+ ## check for tabs - make it nil if no tabs found otherwise use count
91
+ tabs = html.scan( "\t" )
92
+ tabs = tabs.size == 0 ? nil : tabs.size
93
+
94
+
95
+ meta = {
96
+ title: title,
97
+ html_doctype: html_doctype,
98
+ html_charset: html_charset,
99
+ tabs: tabs,
100
+ }
101
+
102
+ meta
103
+ end
104
+
105
+
106
+ end ## class Mirror
107
+ end ## class Webget
@@ -0,0 +1,123 @@
1
+
2
+ ##
3
+ # note - use a sqlite database for caching pages and (internal) links
4
+ ## use via activerecord machinery / object-relational mapper
5
+
6
+
7
+ module MirrorDb
8
+ module Model
9
+
10
+
11
+ class Page < ActiveRecord::Base
12
+ has_many :outgoing_links, class_name: 'Link',
13
+ foreign_key: 'from_page_id',
14
+ :dependent => :delete_all ## :destroy
15
+
16
+ ## use outgoing_pages or linked_pages?
17
+ has_many :linked_pages, :through => :outgoing_links,
18
+ :source => :to_page
19
+
20
+ ## backlink (incoming)
21
+ has_many :incoming_links, class_name: 'Link',
22
+ foreign_key: 'to_page_id',
23
+ :dependent => :delete_all ## :destroy
24
+
25
+ ## use incoming_pages or backlink_pages?
26
+ has_many :backlink_pages, :through => :incoming_links,
27
+ :source => :from_page
28
+
29
+
30
+ def outgoing_paths() linked_pages.pluck(:path); end
31
+ def incoming_paths() backlink_pages.pluck(:path); end
32
+
33
+
34
+ ## find a better name for not cached (was missing)? why? why not?
35
+ ## cached a.k.a. downloaded to local cache
36
+ scope :cached, -> { where( cached: true ) }
37
+ scope :not_cached, -> { where( cached: false ) }
38
+
39
+ ## 404 not_found
40
+ scope :not_found, -> { where( http_status: 404 ) }
41
+ ##
42
+ ## add scope :ok for 200 - why ? why not?
43
+
44
+
45
+ ## for extname (file extensions)
46
+ ## note - .html auto incl .htm !!
47
+ scope :html, -> { where( extname: ['.html', '.htm']) }
48
+ scope :pdf, -> { where( extname: '.pdf') }
49
+
50
+ def html?() extname == '.html' || extname == '.htm'; end
51
+ def not_html?() !html?(); end
52
+ def pdf?() extname == '.pdf'; end
53
+
54
+
55
+ def not_found?() http_status == 404; end
56
+ def not_cached?() !cached?(); end
57
+
58
+
59
+ ### note - path incl. leading slash e.g. /curtour.html
60
+ ## def url() "#{Mirror.config.base_url}#{path}"; end
61
+
62
+
63
+
64
+
65
+ ## tip - Use before_validation instead if needed:
66
+ ## If your callback modifies attributes that need to be validated,
67
+ ## use before_validation instead of before_create.
68
+ ## before_create runs after validation passes
69
+
70
+ ### note - use callback to autofill basename,extname, dirname from path
71
+ before_validation :autofill
72
+
73
+ ## double check added path
74
+ validate :assert_path
75
+
76
+ private
77
+ def autofill
78
+ self.basename = File.basename( path, File.extname( path )) if basename.nil?
79
+ self.extname = File.extname( path ) if extname.nil?
80
+ self.dirname = File.dirname( path ) if dirname.nil?
81
+
82
+ ###
83
+ ## note - always downcase extname for now - why? why not?
84
+ ## possibly .HTM or .HTML (or even .Html or such)
85
+ ##
86
+ ## maybe latter autofill format or such - why? why not?
87
+ self.extname = extname.downcase if extname
88
+ end
89
+
90
+ def assert_path
91
+ ## assert - double check
92
+ ## make sure url.path does NOT start with // or
93
+ ## /// !!
94
+ ## and does NOT end_with /
95
+ ##
96
+ ## page_rec.path.include?( %r{/{2,}} ) ||
97
+ ## fix http.// typos!!!
98
+ ## page_rec.path.match?( %r{\.{2,}} )
99
+ ## fix ..sources typos ...
100
+ ## pages
101
+ if path.start_with?( '//' ) ||
102
+ path.end_with?( '/' ) ||
103
+ !path.start_with?( '/' ) ## note - MUST start with single slash (/)
104
+ errors.add(:path, "broken; starts with // or ends with /")
105
+ end
106
+
107
+ end
108
+ end # class Page
109
+
110
+
111
+
112
+
113
+
114
+ class Link < ActiveRecord::Base
115
+ belongs_to :from_page, class_name: 'Page',
116
+ foreign_key: 'from_page_id'
117
+ belongs_to :to_page, class_name: 'Page',
118
+ foreign_key: 'to_page_id'
119
+ end # class Link
120
+
121
+
122
+ end # module Model
123
+ end # module MirrorDb
@@ -0,0 +1,63 @@
1
+
2
+
3
+ module MirrorDb
4
+ def self.open( path='./mirror.db' )
5
+
6
+ ### reuse connect here !!!
7
+ ### why? why not?
8
+
9
+ config = {
10
+ adapter: 'sqlite3',
11
+ database: path,
12
+ }
13
+
14
+ ActiveRecord::Base.establish_connection( config )
15
+ # ActiveRecord::Base.logger = Logger.new( STDOUT )
16
+
17
+ ## try to speed up sqlite
18
+ ## see http://www.sqlite.org/pragma.html
19
+ con = ActiveRecord::Base.connection
20
+ con.execute( 'PRAGMA synchronous=OFF;' )
21
+ con.execute( 'PRAGMA journal_mode=OFF;' )
22
+ con.execute( 'PRAGMA temp_store=MEMORY;' )
23
+
24
+ ##########################
25
+ ### auto_migrate
26
+ unless Model::Page.table_exists?
27
+ CreateDb.new.up
28
+ end
29
+ end # method open
30
+ end
31
+
32
+
33
+
34
+ __END__
35
+
36
+
37
+ =begin
38
+ def self.open_readonly( path='./mirror.db' )
39
+
40
+ ### raise ArgumentError, "sqlite db #{path} not found" if !File.exist?( path )
41
+
42
+
43
+ config = {
44
+ adapter: 'sqlite3',
45
+ database: path,
46
+ readonly: true, ## try readonly prop!!!
47
+ }
48
+
49
+ ActiveRecord::Base.establish_connection( config )
50
+ # ActiveRecord::Base.logger = Logger.new( STDOUT )
51
+
52
+ ## try to speed up sqlite
53
+ ## see http://www.sqlite.org/pragma.html
54
+ con = ActiveRecord::Base.connection
55
+
56
+ ## add for read-only - why? why not?
57
+ # con.execute( 'PRAGMA query_only=ON;' )
58
+
59
+ # con.execute( 'PRAGMA synchronous=OFF;' )
60
+ # con.execute( 'PRAGMA journal_mode=OFF;' )
61
+ # con.execute( 'PRAGMA temp_store=MEMORY;' )
62
+ end
63
+ =end
@@ -0,0 +1,67 @@
1
+ module MirrorDb
2
+ class CreateDb
3
+ def up
4
+ ActiveRecord::Schema.define do
5
+
6
+ ####
7
+ # pages tables
8
+ create_table :pages do |t|
9
+ t.string :path, null: false
10
+
11
+ ## split/break up path - maybe make it later virtual columns - why? why not?
12
+ ## add basename, dirname, extname - why? why not?
13
+ t.string :basename, null: false
14
+ t.string :dirname, null: false
15
+ t.string :extname, null: false ## note - empty string if no extname?
16
+ ## if present starts with dot (.) e.g.
17
+ ## .html|.htm, .pdf, etc.
18
+ ## keep dot why? why not?
19
+
20
+ t.string :title ## html <title></title>
21
+ t.date :updated ## iso date e.g. (2026-04-29) via web page source
22
+
23
+ ################
24
+ ### add (charset) encoding stuff
25
+ t.string :encoding ## "upstream" text encoding
26
+ ## all pages ALWAYS converted to utf-8
27
+ t.string :encoding_source ## e.g. bom|http|html|user|fallback
28
+
29
+ t.boolean :encoding_valid ## uses String#encoding_valid?
30
+ t.boolean :ascii7bit ## uses String#ascii_only? check if all chars are ascii 7bit (utf8-compatible) ??
31
+ t.integer :chars_8bit ## count of 8bit (126-255) chars - nil|0|1|2|etc.
32
+ t.integer :utf8_replace ## count invalid/replace chars in utf8 - nil|0|1|2
33
+ t.integer :tabs ## count tabs/tabstops in html source (use tab or tabs ??)
34
+
35
+
36
+ t.string :html_doctype
37
+ t.string :html_charset
38
+
39
+ t.string :http_content_type ## http content-type header
40
+ t.integer :http_content_length ## http content-length header
41
+ t.integer :http_status ## e.g. 200, 404 - make mandatory - why? why not?
42
+
43
+ ## or use download or date (fetched) or such??
44
+ t.boolean :cached, default: false
45
+
46
+ # t.timestamps ## (auto)add - why? why not?
47
+ # do NOT use; save space for now - auto-generated db is read-only
48
+ end
49
+ add_index :pages, :path, unique: true
50
+
51
+
52
+ ## join table - no need for own ids
53
+ create_table :links, id: false do |t|
54
+ t.integer :from_page_id, null: false
55
+ t.integer :to_page_id, null: false
56
+ end
57
+ add_index :links, [:from_page_id, :to_page_id], unique: true
58
+ add_index :links, :from_page_id
59
+ add_index :links, :to_page_id
60
+
61
+ ##
62
+ ## add errors or log or such - why? why not?
63
+ ##
64
+ end # Schema.define
65
+ end # method up
66
+ end # class CreateDb
67
+ end # module MirrorDb
@@ -0,0 +1,83 @@
1
+
2
+
3
+ class Webget
4
+ class Mirror
5
+
6
+
7
+ def _download_page( url,
8
+ encoding: nil,
9
+ force: false )
10
+
11
+ ##
12
+ ## note - on windows cached will be CASE-INSENSITIVE
13
+ ## e.g. usadave and USAdave will match
14
+ ## make sure match is CASE-SENSITIVE!!!
15
+
16
+ if force == false && Webcache.cached?( url )
17
+ puts " CACHE HIT - #{url}"
18
+ html = Webcache.read( url )
19
+
20
+ return [html,nil]
21
+ end
22
+
23
+
24
+ puts "==> download #{url} (encoding: #{encoding})..."
25
+
26
+
27
+ ## note: assume plain 7-bit ascii for now
28
+ ## -- assume rsssf uses ISO_8859_15 (updated version of ISO_8859_1)
29
+ ###-- does NOT use utf-8 character encoding!!!
30
+ response = Webget.page( url, encoding: encoding ) ## fetch (and cache) html page (via HTTP GET)
31
+
32
+ ## note: exit on get / fetch error - do NOT continue for now - why? why not?
33
+ ## note - allow 404 not found to pass through
34
+
35
+
36
+ ## note - status.code is an integer number (not a string!!)
37
+ if response.status.code == 404
38
+
39
+ meta = {
40
+ http_status: response.status.code,
41
+ }
42
+
43
+ ["404 NOT FOUND",meta]
44
+
45
+ elsif response.status.code == 200
46
+ puts "html:"
47
+ html = response.text
48
+ pp html[0..200]
49
+
50
+ ## note - use "hacky" undocument internal response._text_encoding
51
+ ## to get "upstream" encoding used from convert to utf-8
52
+ ## unicode boms may override user supplied encoding!!!
53
+ ## or change upstream
54
+ ## and use/add response.text_with_encoding( ) - why? why not?
55
+ ## yes, upstream now uses
56
+ ## text( encoding: _encoding_user )!!!
57
+
58
+
59
+ meta = {
60
+ encoding: response._text_encoding,
61
+ encoding_source: response._text_encoding_source, ## bom|http|html|user|fallback
62
+ encoding_valid: response._text_encoding_valid,
63
+
64
+ ascii7bit: response._text_ascii_only,
65
+ chars_8bit: response._text_8bit,
66
+ utf8_replace: response._text_utf8_replace,
67
+
68
+ http_content_type: response.content_type,
69
+ http_content_length: response.content_length,
70
+ http_status: response.status.code,
71
+ }
72
+
73
+ [html,meta]
74
+
75
+ else
76
+ puts "unexpected http status (code) - #{response.status}"
77
+ exit 1
78
+ end
79
+ end
80
+
81
+
82
+ end ## class Mirror
83
+ end ## class Webget