relaton 3.0.0.pre.alpha.1 → 3.0.0.pre.alpha.2
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 +4 -4
- data/README.adoc +57 -1
- data/bin/console +0 -1
- data/lib/relaton/3gpp/bibliography.rb +82 -7
- data/lib/relaton/3gpp/data_fetcher.rb +51 -3
- data/lib/relaton/3gpp/docidentifier.rb +114 -0
- data/lib/relaton/3gpp/item.rb +6 -0
- data/lib/relaton/3gpp/parser.rb +1 -1
- data/lib/relaton/3gpp/processor.rb +4 -1
- data/lib/relaton/3gpp.rb +5 -1
- data/lib/relaton/adobe/bibdata.rb +8 -0
- data/lib/relaton/adobe/bibitem.rb +8 -0
- data/lib/relaton/adobe/bibliography.rb +92 -0
- data/lib/relaton/adobe/docidentifier.rb +49 -0
- data/lib/relaton/adobe/doctype.rb +14 -0
- data/lib/relaton/adobe/ext.rb +32 -0
- data/lib/relaton/adobe/item.rb +15 -0
- data/lib/relaton/adobe/item_base.rb +18 -0
- data/lib/relaton/adobe/item_data.rb +6 -0
- data/lib/relaton/adobe/processor.rb +45 -0
- data/lib/relaton/adobe/util.rb +8 -0
- data/lib/relaton/adobe.rb +37 -0
- data/lib/relaton/bib/model/address.rb +2 -2
- data/lib/relaton/bib/model/docidentifier.rb +24 -9
- data/lib/relaton/bib/model/localized_string.rb +1 -1
- data/lib/relaton/bib/model/structured_identifier.rb +10 -9
- data/lib/relaton/bib/sanitizer.rb +202 -6
- data/lib/relaton/bib.rb +0 -2
- data/lib/relaton/bipm/bibliography.rb +159 -10
- data/lib/relaton/bipm/data_fetcher.rb +26 -2
- data/lib/relaton/bipm/data_outcomes_parser.rb +3 -4
- data/lib/relaton/bipm/id_parser.rb +5 -4
- data/lib/relaton/bipm/model/structured_identifier.rb +21 -0
- data/lib/relaton/bipm/processor.rb +2 -2
- data/lib/relaton/bipm/rawdata_bipm_metrologia/fetcher.rb +2 -4
- data/lib/relaton/bipm/si_brochure_parser.rb +5 -3
- data/lib/relaton/bipm.rb +7 -1
- data/lib/relaton/bsi/bibliography.rb +115 -43
- data/lib/relaton/bsi/hit.rb +14 -0
- data/lib/relaton/bsi/hit_collection.rb +15 -16
- data/lib/relaton/bsi/model/docidentifier.rb +99 -1
- data/lib/relaton/bsi/processor.rb +1 -0
- data/lib/relaton/calconnect/bibliography.rb +12 -14
- data/lib/relaton/calconnect/data_fetcher.rb +77 -9
- data/lib/relaton/calconnect/docidentifier.rb +80 -0
- data/lib/relaton/calconnect/hit_collection.rb +65 -57
- data/lib/relaton/calconnect/model/item.rb +7 -0
- data/lib/relaton/calconnect/processor.rb +7 -1
- data/lib/relaton/calconnect.rb +11 -1
- data/lib/relaton/ccsds/data/fetcher.rb +17 -12
- data/lib/relaton/ccsds/data/parser.rb +1 -1
- data/lib/relaton/ccsds/hit_collection.rb +6 -1
- data/lib/relaton/ccsds/model/docidentifier.rb +121 -0
- data/lib/relaton/ccsds/model/item.rb +2 -0
- data/lib/relaton/cen/bibliography.rb +75 -47
- data/lib/relaton/cen/hit.rb +16 -1
- data/lib/relaton/cen/hit_collection.rb +59 -11
- data/lib/relaton/cen/model/docidentifier.rb +92 -1
- data/lib/relaton/cen/processor.rb +13 -8
- data/lib/relaton/cen/scraper.rb +13 -5
- data/lib/relaton/cen.rb +1 -0
- data/lib/relaton/cie/data_fetcher.rb +215 -30
- data/lib/relaton/cie/processor.rb +3 -1
- data/lib/relaton/cie/scrapper.rb +15 -2
- data/lib/relaton/cie.rb +2 -1
- data/lib/relaton/core/data_fetcher.rb +150 -3
- data/lib/relaton/core/governor.rb +320 -0
- data/lib/relaton/core/pacer.rb +134 -0
- data/lib/relaton/core/processor.rb +19 -0
- data/lib/relaton/core/request_error.rb +14 -0
- data/lib/relaton/core.rb +3 -0
- data/lib/relaton/db/registry.rb +41 -1
- data/lib/relaton/doi/crossref.rb +19 -2
- data/lib/relaton/doi/parser.rb +109 -15
- data/lib/relaton/easc/bibdata.rb +8 -0
- data/lib/relaton/easc/bibitem.rb +8 -0
- data/lib/relaton/easc/bibliography.rb +95 -0
- data/lib/relaton/easc/docidentifier.rb +100 -0
- data/lib/relaton/easc/doctype.rb +14 -0
- data/lib/relaton/easc/ext.rb +44 -0
- data/lib/relaton/easc/item.rb +13 -0
- data/lib/relaton/easc/item_base.rb +18 -0
- data/lib/relaton/easc/item_data.rb +6 -0
- data/lib/relaton/easc/processor.rb +46 -0
- data/lib/relaton/easc/util.rb +8 -0
- data/lib/relaton/easc.rb +35 -0
- data/lib/relaton/ecma/bibliography.rb +93 -25
- data/lib/relaton/ecma/data_fetcher.rb +71 -12
- data/lib/relaton/ecma/docidentifier.rb +124 -0
- data/lib/relaton/ecma/item.rb +2 -0
- data/lib/relaton/ecma/memento_parser.rb +1 -1
- data/lib/relaton/ecma/page_fetcher.rb +15 -3
- data/lib/relaton/ecma/parser_common.rb +2 -2
- data/lib/relaton/ecma/processor.rb +4 -1
- data/lib/relaton/ecma/standard_parser.rb +2 -2
- data/lib/relaton/ecma.rb +10 -1
- data/lib/relaton/etsi/bibliography.rb +67 -2
- data/lib/relaton/etsi/data_fetcher.rb +43 -4
- data/lib/relaton/etsi/processor.rb +3 -1
- data/lib/relaton/etsi.rb +2 -1
- data/lib/relaton/gb/bibliography.rb +55 -29
- data/lib/relaton/gb/docidentifier.rb +58 -9
- data/lib/relaton/gb/processor.rb +3 -0
- data/lib/relaton/gb/scraper.rb +27 -10
- data/lib/relaton/gost/bibdata.rb +8 -0
- data/lib/relaton/gost/bibitem.rb +8 -0
- data/lib/relaton/gost/bibliography.rb +107 -0
- data/lib/relaton/gost/docidentifier.rb +80 -0
- data/lib/relaton/gost/doctype.rb +16 -0
- data/lib/relaton/gost/ext.rb +46 -0
- data/lib/relaton/gost/item.rb +15 -0
- data/lib/relaton/gost/item_base.rb +18 -0
- data/lib/relaton/gost/item_data.rb +6 -0
- data/lib/relaton/gost/processor.rb +49 -0
- data/lib/relaton/gost/util.rb +8 -0
- data/lib/relaton/gost.rb +36 -0
- data/lib/relaton/iala/bibdata.rb +8 -0
- data/lib/relaton/iala/bibitem.rb +8 -0
- data/lib/relaton/iala/bibliography.rb +146 -0
- data/lib/relaton/iala/docidentifier.rb +89 -0
- data/lib/relaton/iala/doctype.rb +18 -0
- data/lib/relaton/iala/ext.rb +32 -0
- data/lib/relaton/iala/item.rb +21 -0
- data/lib/relaton/iala/item_base.rb +18 -0
- data/lib/relaton/iala/item_data.rb +6 -0
- data/lib/relaton/iala/processor.rb +43 -0
- data/lib/relaton/iala/relation.rb +7 -0
- data/lib/relaton/iala/util.rb +8 -0
- data/lib/relaton/iala.rb +35 -0
- data/lib/relaton/iana/bibliography.rb +67 -14
- data/lib/relaton/iana/data_fetcher.rb +35 -5
- data/lib/relaton/iana/processor.rb +3 -1
- data/lib/relaton/iana.rb +12 -1
- data/lib/relaton/iec/data_fetcher.rb +7 -1
- data/lib/relaton/iec/hit_collection.rb +1 -1
- data/lib/relaton/iec/model/docidentifier.rb +9 -5
- data/lib/relaton/iec/model/ext.rb +2 -2
- data/lib/relaton/iec/processor.rb +1 -0
- data/lib/relaton/ieee/bibliography.rb +25 -3
- data/lib/relaton/ieee/data_fetcher.rb +158 -17
- data/lib/relaton/ieee/idams_parser.rb +18 -11
- data/lib/relaton/ieee/processor.rb +4 -1
- data/lib/relaton/ieee/rawbib_id_parser.rb +291 -86
- data/lib/relaton/ieee.rb +2 -1
- data/lib/relaton/ietf/data_fetcher.rb +295 -12
- data/lib/relaton/ietf/processor.rb +7 -3
- data/lib/relaton/ietf/rfc/entry.rb +39 -3
- data/lib/relaton/ietf/scraper.rb +69 -36
- data/lib/relaton/ietf.rb +4 -1
- data/lib/relaton/iho/bibliography.rb +1 -1
- data/lib/relaton/iho/docidentifier.rb +1 -1
- data/lib/relaton/index/file_io.rb +11 -11
- data/lib/relaton/index/file_storage.rb +6 -1
- data/lib/relaton/index/pool.rb +6 -1
- data/lib/relaton/index/shard_source.rb +201 -0
- data/lib/relaton/index/type.rb +63 -12
- data/lib/relaton/index.rb +2 -1
- data/lib/relaton/iso/bibliography.rb +20 -15
- data/lib/relaton/iso/data_fetcher.rb +3 -3
- data/lib/relaton/iso/data_parser.rb +17 -3
- data/lib/relaton/iso/hit_collection.rb +27 -15
- data/lib/relaton/iso/item_data.rb +22 -0
- data/lib/relaton/iso/model/docidentifier.rb +24 -12
- data/lib/relaton/iso/processor.rb +1 -0
- data/lib/relaton/iso/scraper.rb +19 -3
- data/lib/relaton/itu/bibliography.rb +9 -4
- data/lib/relaton/itu/data_crawler_r.rb +664 -0
- data/lib/relaton/itu/data_fetcher.rb +496 -50
- data/lib/relaton/itu/data_merge_r.rb +149 -0
- data/lib/relaton/itu/data_parser_r.rb +163 -89
- data/lib/relaton/itu/data_parser_t.rb +228 -0
- data/lib/relaton/itu/family_cache.rb +177 -0
- data/lib/relaton/itu/governor.rb +56 -0
- data/lib/relaton/itu/hit.rb +9 -3
- data/lib/relaton/itu/hit_collection.rb +258 -86
- data/lib/relaton/itu/model/docidentifier.rb +67 -1
- data/lib/relaton/itu/model/structured_identifier.rb +19 -0
- data/lib/relaton/itu/processor.rb +10 -4
- data/lib/relaton/itu/pubid.rb +27 -5
- data/lib/relaton/itu/recommendation_fields.rb +334 -0
- data/lib/relaton/itu/recommendation_parser.rb +18 -149
- data/lib/relaton/itu/scraper.rb +13 -3
- data/lib/relaton/itu.rb +2 -1
- data/lib/relaton/jcgm/bibdata.rb +8 -0
- data/lib/relaton/jcgm/bibitem.rb +8 -0
- data/lib/relaton/jcgm/bibliography.rb +97 -0
- data/lib/relaton/jcgm/data_fetcher.rb +81 -0
- data/lib/relaton/jcgm/docidentifier.rb +102 -0
- data/lib/relaton/jcgm/doctype.rb +12 -0
- data/lib/relaton/jcgm/ext.rb +23 -0
- data/lib/relaton/jcgm/item.rb +20 -0
- data/lib/relaton/jcgm/item_base.rb +18 -0
- data/lib/relaton/jcgm/item_data.rb +6 -0
- data/lib/relaton/jcgm/meetings_parser.rb +175 -0
- data/lib/relaton/jcgm/processor.rb +71 -0
- data/lib/relaton/jcgm/relation.rb +9 -0
- data/lib/relaton/jcgm/structured_identifier.rb +40 -0
- data/lib/relaton/jcgm/util.rb +8 -0
- data/lib/relaton/jcgm.rb +24 -0
- data/lib/relaton/jis/bibliography.rb +8 -10
- data/lib/relaton/jis/data_fetcher.rb +21 -19
- data/lib/relaton/jis/docidentifier.rb +104 -5
- data/lib/relaton/jis/hit.rb +18 -23
- data/lib/relaton/jis/hit_collection.rb +19 -18
- data/lib/relaton/jis/processor.rb +1 -1
- data/lib/relaton/jis.rb +2 -3
- data/lib/relaton/logger/channels/gh_issue.rb +78 -13
- data/lib/relaton/nist/data_fetcher.rb +63 -13
- data/lib/relaton/nist/docidentifier.rb +165 -0
- data/lib/relaton/nist/item.rb +2 -0
- data/lib/relaton/nist/item_base.rb +16 -0
- data/lib/relaton/nist/mods_parser.rb +38 -12
- data/lib/relaton/nist/processor.rb +2 -1
- data/lib/relaton/nist/relation.rb +3 -0
- data/lib/relaton/nist/scraper.rb +6 -3
- data/lib/relaton/oasis/bibliography.rb +147 -6
- data/lib/relaton/oasis/data_fetcher.rb +41 -5
- data/lib/relaton/oasis/data_parser_utils.rb +37 -3
- data/lib/relaton/oasis/docidentifier.rb +54 -0
- data/lib/relaton/oasis/item.rb +3 -0
- data/lib/relaton/oasis/processor.rb +7 -1
- data/lib/relaton/oasis.rb +14 -1
- data/lib/relaton/ogc/data_fetcher.rb +23 -2
- data/lib/relaton/ogc/docidentifier.rb +105 -0
- data/lib/relaton/ogc/hit_collection.rb +78 -3
- data/lib/relaton/ogc/processor.rb +2 -1
- data/lib/relaton/ogc.rb +5 -1
- data/lib/relaton/oiml/bibliography.rb +90 -15
- data/lib/relaton/oiml/docidentifier.rb +18 -3
- data/lib/relaton/omg/docidentifier.rb +67 -0
- data/lib/relaton/omg/item.rb +1 -0
- data/lib/relaton/omg/processor.rb +1 -0
- data/lib/relaton/omg/scraper.rb +61 -16
- data/lib/relaton/omg.rb +1 -0
- data/lib/relaton/plateau/bibliography.rb +10 -3
- data/lib/relaton/plateau/data_fetcher.rb +25 -2
- data/lib/relaton/plateau/handbook_parser.rb +8 -1
- data/lib/relaton/plateau/hit.rb +10 -2
- data/lib/relaton/plateau/hit_collection.rb +31 -11
- data/lib/relaton/plateau/processor.rb +3 -1
- data/lib/relaton/plateau/technical_report_parser.rb +8 -1
- data/lib/relaton/plateau.rb +2 -1
- data/lib/relaton/sdo/config.rb +34 -0
- data/lib/relaton/sdo/fetcher.rb +52 -0
- data/lib/relaton/sdo/logo.rb +95 -0
- data/lib/relaton/sdo/name.rb +26 -0
- data/lib/relaton/sdo/organization.rb +71 -0
- data/lib/relaton/sdo/store.rb +49 -0
- data/lib/relaton/sdo.rb +29 -0
- data/lib/relaton/version.rb +1 -1
- data/lib/relaton/w3c/bibliography.rb +132 -12
- data/lib/relaton/w3c/data_fetcher.rb +194 -16
- data/lib/relaton/w3c/data_parser.rb +3 -3
- data/lib/relaton/w3c/docidentifier.rb +48 -0
- data/lib/relaton/w3c/governor.rb +32 -0
- data/lib/relaton/w3c/item.rb +3 -0
- data/lib/relaton/w3c/pubid.rb +12 -0
- data/lib/relaton/w3c/safe_realize.rb +110 -21
- data/lib/relaton/w3c.rb +12 -1
- data/lib/relaton/xsf/bibliography.rb +61 -1
- data/lib/relaton/xsf/data_fetcher.rb +55 -5
- data/lib/relaton/xsf/docidentifier.rb +46 -0
- data/lib/relaton/xsf/hit_collection.rb +31 -3
- data/lib/relaton/xsf/item.rb +6 -0
- data/lib/relaton/xsf/processor.rb +1 -0
- data/lib/relaton/xsf.rb +5 -1
- data/lib/relaton.rb +42 -0
- metadata +135 -24
- data/lib/relaton/ieee/pub_id.rb +0 -161
- data/lib/relaton/index/id_number.rb +0 -30
data/lib/relaton/cen/scraper.rb
CHANGED
|
@@ -75,12 +75,20 @@ module Relaton
|
|
|
75
75
|
Bib::Status.new(stage: stage)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
#
|
|
78
|
+
# The number and part come from the pubid. `#root` is the accessor that
|
|
79
|
+
# answers for every form: an adopted norm keeps both on the adopted ISO
|
|
80
|
+
# document, and a supplement keeps both on the document it supplements.
|
|
81
|
+
# pubid holds a sub-part inside `part` (`61375-2-3` gives `"2-3"`), so
|
|
82
|
+
# the separator swap reproduces the old `"2:3"`.
|
|
83
|
+
#
|
|
84
|
+
# @param hit [Relaton::Cen::Hit]
|
|
79
85
|
# @return [Relaton::Bib::StructuredIdentifier]
|
|
80
86
|
def fetch_structuredid(hit)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
root = hit.pubid&.root
|
|
88
|
+
docnumber = root&.number&.to_s
|
|
89
|
+
partnumber = root&.part.to_s.split("-").join(":")
|
|
90
|
+
StructuredIdentifier.new(docnumber: docnumber, partnumber: partnumber,
|
|
91
|
+
agency: ["CEN"])
|
|
84
92
|
end
|
|
85
93
|
|
|
86
94
|
# Fetch relations.
|
|
@@ -201,7 +209,7 @@ module Relaton
|
|
|
201
209
|
doctype: Bib::Doctype.new(content: "international-standard"),
|
|
202
210
|
flavor: "cen",
|
|
203
211
|
ics: fetch_ics(doc),
|
|
204
|
-
structuredidentifier: fetch_structuredid(hit
|
|
212
|
+
structuredidentifier: fetch_structuredid(hit),
|
|
205
213
|
)
|
|
206
214
|
end
|
|
207
215
|
end
|
data/lib/relaton/cen.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "English"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "ferrum"
|
|
6
6
|
require "nokogiri"
|
|
7
|
+
require "pubid"
|
|
7
8
|
require "relaton/index"
|
|
8
9
|
require "relaton/bib"
|
|
9
10
|
require "relaton/core/data_fetcher"
|
|
@@ -21,6 +22,12 @@ module Relaton
|
|
|
21
22
|
CHALLENGE_MARKERS = ["Just a moment", "challenge-platform"].freeze
|
|
22
23
|
MAX_CHALLENGE_WAIT = 30
|
|
23
24
|
|
|
25
|
+
# Raised when the Cloudflare challenge ("Just a moment…") is still showing
|
|
26
|
+
# after MAX_CHALLENGE_WAIT seconds. It's a retriable signal (see
|
|
27
|
+
# DataFetcher::RETRIABLE_ERRORS): rather than parse the challenge HTML as a
|
|
28
|
+
# document, the caller (#time_req) backs the worker off and retries.
|
|
29
|
+
class ChallengeError < StandardError; end
|
|
30
|
+
|
|
24
31
|
def initialize
|
|
25
32
|
@browser = Ferrum::Browser.new(
|
|
26
33
|
headless: true,
|
|
@@ -68,18 +75,106 @@ module Relaton
|
|
|
68
75
|
|
|
69
76
|
sleep 1
|
|
70
77
|
end
|
|
78
|
+
raise ChallengeError, "Cloudflare challenge did not clear after #{MAX_CHALLENGE_WAIT}s"
|
|
71
79
|
end
|
|
72
80
|
end
|
|
73
81
|
|
|
74
82
|
class DataFetcher < Relaton::Core::DataFetcher
|
|
75
83
|
URL = "https://www.techstreet.com/cie/searches/31156444?page=1&per_page=100"
|
|
76
84
|
|
|
85
|
+
# Default worker-pool size for the parallel detail-fetch phase. Kept small
|
|
86
|
+
# so N stealth Chrome instances don't burst requests fast enough to trip
|
|
87
|
+
# Cloudflare; tune via RELATON_CIE_CONCURRENCY (see .concurrency).
|
|
88
|
+
DEFAULT_CONCURRENCY = 5
|
|
89
|
+
|
|
90
|
+
# Per-worker pacing bounds (seconds). Each worker starts at BASE_GAP and
|
|
91
|
+
# doubles its own gap up to MAX_GAP on trouble (see Pacing / #time_req).
|
|
92
|
+
BASE_GAP = 1
|
|
93
|
+
MAX_GAP = 32
|
|
94
|
+
|
|
95
|
+
# Number of detail-fetch worker threads. Tunable via env var so
|
|
96
|
+
# relaton-data-cie's crawler workflow can dial it up for speed or down to
|
|
97
|
+
# lighten load on techstreet without a code change. Never below 1.
|
|
98
|
+
def self.concurrency
|
|
99
|
+
[(ENV["RELATON_CIE_CONCURRENCY"] || DEFAULT_CONCURRENCY).to_i, 1].max
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def initialize(output, format)
|
|
103
|
+
super
|
|
104
|
+
# Guards the shared bookkeeping (@files, @errors, @seen) and the index
|
|
105
|
+
# mutation while workers fetch detail pages in parallel. The slow
|
|
106
|
+
# agent.get runs outside this lock; only the cheap build+write is held.
|
|
107
|
+
@mutex = Mutex.new
|
|
108
|
+
# output file => catalogue position of the hit that last claimed it, so a
|
|
109
|
+
# duplicate primary id resolves to the same last-by-position winner the
|
|
110
|
+
# serial crawl would pick, regardless of worker completion order.
|
|
111
|
+
@seen = {}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Factory for a fresh stealth browser agent. The pool builds one per worker
|
|
115
|
+
# (each its own Chrome, each with the full UA/header/`navigator` masking);
|
|
116
|
+
# #agent memoizes a single one for the serial listing phase. Specs stub
|
|
117
|
+
# this so one double can back every worker.
|
|
118
|
+
def build_agent
|
|
119
|
+
BrowserAgent.new
|
|
120
|
+
end
|
|
121
|
+
|
|
77
122
|
def agent
|
|
78
|
-
@agent ||=
|
|
123
|
+
@agent ||= build_agent
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Per-worker adaptive request pacing. Replaces the old single global 4 s
|
|
127
|
+
# gap: each worker owns one Pacing, starts at BASE_GAP, and doubles its gap
|
|
128
|
+
# (capped at MAX_GAP) whenever a request hits trouble (a Cloudflare
|
|
129
|
+
# challenge or a Ferrum/socket error), so a struggling worker slows itself
|
|
130
|
+
# without throttling the healthy ones.
|
|
131
|
+
class Pacing
|
|
132
|
+
def initialize(base: BASE_GAP, max: MAX_GAP)
|
|
133
|
+
@base = base
|
|
134
|
+
@max = max
|
|
135
|
+
@gap = base
|
|
136
|
+
@last = nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
attr_reader :gap
|
|
140
|
+
|
|
141
|
+
# Sleep off whatever remains of this worker's gap since its last request,
|
|
142
|
+
# then stamp the clock (in ensure, mirroring the old #time_req).
|
|
143
|
+
def throttle
|
|
144
|
+
sleep [@gap - (Time.now - @last), 0].max if @last
|
|
145
|
+
ensure
|
|
146
|
+
@last = Time.now
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Exponential backoff, capped at @max. Called before a retry on trouble.
|
|
150
|
+
def backoff
|
|
151
|
+
@gap = [@gap * 2, @max].min
|
|
152
|
+
end
|
|
79
153
|
end
|
|
80
154
|
|
|
81
155
|
def index
|
|
82
|
-
@index ||= Index.find_or_create :cie, file: "
|
|
156
|
+
@index ||= Index.find_or_create :cie, file: "#{INDEXFILE}.yaml",
|
|
157
|
+
pubid_class: ::Pubid::Cie::Identifier
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Parse a docidentifier string into a Pubid::Cie::Identifier, or nil if
|
|
161
|
+
# pubid can't parse it or it won't survive the structured index — so a
|
|
162
|
+
# single bad id never aborts the crawl or corrupts index-v2. The caller
|
|
163
|
+
# (#write_file) records the skip.
|
|
164
|
+
#
|
|
165
|
+
# The guard mirrors the read side's acceptance test
|
|
166
|
+
# (Index::FileIO#id_supported?, `from_hash(to_hash) == to_hash`): the
|
|
167
|
+
# loader raises InvalidIndexError and rejects the WHOLE index on the first
|
|
168
|
+
# id that doesn't round-trip, so an id that serializes but can't be
|
|
169
|
+
# deserialized back must be dropped here rather than poison every lookup.
|
|
170
|
+
def pubid(id)
|
|
171
|
+
pid = ::Pubid::Cie.parse id
|
|
172
|
+
hash = pid.to_hash
|
|
173
|
+
return nil unless ::Pubid::Cie::Identifier.from_hash(hash).to_hash == hash
|
|
174
|
+
|
|
175
|
+
pid
|
|
176
|
+
rescue StandardError
|
|
177
|
+
nil
|
|
83
178
|
end
|
|
84
179
|
|
|
85
180
|
def log_error(msg)
|
|
@@ -273,9 +368,24 @@ module Relaton
|
|
|
273
368
|
end
|
|
274
369
|
|
|
275
370
|
# @param bib [RelatonCie::BibliographicItem]
|
|
276
|
-
|
|
371
|
+
# @param pos [Integer, nil] the hit's catalogue position, used only by the
|
|
372
|
+
# parallel pool to keep a deterministic last-by-position winner when two
|
|
373
|
+
# hits map to the same output file. nil (direct calls) keeps the
|
|
374
|
+
# historical warn-and-overwrite behavior.
|
|
375
|
+
def write_file(bib, pos = nil) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
277
376
|
id = bib.docidentifier[0].content
|
|
278
377
|
file = output_file id
|
|
378
|
+
# Index every distinct id (the serial crawl indexes each one, even two
|
|
379
|
+
# that resolve to the same file), before the content-dedup gate below.
|
|
380
|
+
pid = pubid id
|
|
381
|
+
if pid
|
|
382
|
+
index.add_or_update pid, file
|
|
383
|
+
else
|
|
384
|
+
Util.warn { "Unparseable id `#{id}` was not indexed (#{file})" }
|
|
385
|
+
end
|
|
386
|
+
# Only the last-by-position hit writes the shared file's content.
|
|
387
|
+
return if superseded? file, pos
|
|
388
|
+
|
|
279
389
|
if @files.include? file
|
|
280
390
|
Util.warn do
|
|
281
391
|
"File #{file} exists. Docid: #{bib.docidentifier[0].content}\n" \
|
|
@@ -283,71 +393,146 @@ module Relaton
|
|
|
283
393
|
end
|
|
284
394
|
else @files << file
|
|
285
395
|
end
|
|
286
|
-
index.add_or_update bib.docidentifier[0].content, file
|
|
287
396
|
File.write file, serialize(bib), encoding: "UTF-8"
|
|
288
397
|
end
|
|
289
398
|
|
|
399
|
+
# True when another hit with a *later* catalogue position has already
|
|
400
|
+
# claimed `file`. The serial crawl is last-writer-wins, so that later hit
|
|
401
|
+
# is canonical and this (earlier or duplicate) content write must be
|
|
402
|
+
# dropped — making the output independent of worker completion order. With
|
|
403
|
+
# pos nil there's no position to compare, so nothing is superseded.
|
|
404
|
+
def superseded?(file, pos)
|
|
405
|
+
return false if pos.nil?
|
|
406
|
+
|
|
407
|
+
prev = @seen[file]
|
|
408
|
+
return true if prev && prev >= pos
|
|
409
|
+
|
|
410
|
+
@seen[file] = pos
|
|
411
|
+
false
|
|
412
|
+
end
|
|
413
|
+
|
|
290
414
|
def to_xml(bib) = bib.to_xml(bibdata: true)
|
|
291
415
|
def to_yaml(bib) = bib.to_yaml
|
|
292
416
|
def to_bibxml(bib) = bib.to_rfcxml
|
|
293
417
|
|
|
418
|
+
# Fetch and store one document. The slow detail-page load
|
|
419
|
+
# (`worker_agent.get`, throttled by this worker's `pacing`) runs outside
|
|
420
|
+
# the lock — that's the parallel win; the cheap build + write is done under
|
|
421
|
+
# @mutex so @files/@errors/@seen and the index stay consistent across
|
|
422
|
+
# workers. Called directly (single stubbed agent) by specs and from the
|
|
423
|
+
# pool with a per-worker agent/pacing/position.
|
|
424
|
+
#
|
|
294
425
|
# @param hit [Nokogiri::HTML::Element]
|
|
295
|
-
|
|
426
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
427
|
+
def parse_page(hit, pos = nil, worker_agent = agent, pacing = nil)
|
|
296
428
|
url = hit.at('h3/a')[:href]
|
|
297
|
-
doc = time_req {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
429
|
+
doc = time_req(pacing) { worker_agent.get url }
|
|
430
|
+
@mutex.synchronize do
|
|
431
|
+
item = ItemData.new(
|
|
432
|
+
type: "standard", source: fetch_source(url), docnumber: fetch_docnumber(hit),
|
|
433
|
+
docidentifier: fetch_docid(hit, doc), title: fetch_title(doc),
|
|
434
|
+
abstract: fetch_abstract(doc), date: fetch_date(doc),
|
|
435
|
+
edition: fetch_edition(doc), contributor: fetch_contributor(doc),
|
|
436
|
+
relation: fetch_relation(doc), language: "en", script: "Latn",
|
|
437
|
+
ext: fetch_ext
|
|
438
|
+
)
|
|
439
|
+
write_file item, pos
|
|
440
|
+
end
|
|
307
441
|
rescue StandardError => e
|
|
308
442
|
Util.error do
|
|
309
443
|
"Document: #{url}\n#{e.message}\n#{e.backtrace}"
|
|
310
444
|
end
|
|
311
445
|
end
|
|
446
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
312
447
|
|
|
313
448
|
def fetch(_source = nil)
|
|
314
|
-
|
|
449
|
+
process_hits collect_hits
|
|
450
|
+
index.save
|
|
315
451
|
report_errors
|
|
316
452
|
ensure
|
|
317
453
|
@agent&.quit
|
|
318
454
|
end
|
|
319
455
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
456
|
+
# Phase 1 (serial): walk the search-result pages, following the "next"
|
|
457
|
+
# link, and collect every //li[@data-product] hit into one list (~12 page
|
|
458
|
+
# loads at per_page=100, so serial pacing here is cheap). The detail-page
|
|
459
|
+
# fetches happen in #process_hits.
|
|
460
|
+
def collect_hits(url = URL, hits = [])
|
|
461
|
+
result = time_req(listing_pacing) { agent.get url }
|
|
462
|
+
hits.concat result.xpath("//li[@data-product]").to_a
|
|
323
463
|
np = result.at '//a[@class="next_page"]'
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
464
|
+
return hits unless np
|
|
465
|
+
|
|
466
|
+
next_href = np[:href]
|
|
467
|
+
next_url = next_href.start_with?("http") ? next_href : "https://www.techstreet.com#{next_href}"
|
|
468
|
+
collect_hits next_url, hits
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Phase 2 (parallel): fan the collected hits out across a bounded worker
|
|
472
|
+
# pool. Each worker owns its own stealth browser agent and its own adaptive
|
|
473
|
+
# pacing. The agents are built up front on this thread so a Chrome-launch
|
|
474
|
+
# failure aborts fast (before any hit is enqueued and before the index is
|
|
475
|
+
# saved) rather than leaving a dead worker that can't drain the queue; and
|
|
476
|
+
# every created agent is quit in the ensure even if enqueuing or a worker
|
|
477
|
+
# raises. Hits carry their catalogue position so #write_file keeps a
|
|
478
|
+
# deterministic winner for any duplicate output file — byte-identical
|
|
479
|
+
# output regardless of completion order.
|
|
480
|
+
def process_hits(hits)
|
|
481
|
+
return if hits.empty?
|
|
482
|
+
|
|
483
|
+
n = self.class.concurrency
|
|
484
|
+
agents = []
|
|
485
|
+
queue = SizedQueue.new(n * 2)
|
|
486
|
+
n.times { agents << build_agent }
|
|
487
|
+
workers = agents.map { |worker_agent| spawn_worker(queue, worker_agent) }
|
|
488
|
+
hits.each_with_index { |hit, pos| queue << [hit, pos] }
|
|
489
|
+
n.times { queue << nil } # poison pills
|
|
490
|
+
workers.each(&:join)
|
|
491
|
+
ensure
|
|
492
|
+
agents&.each(&:quit)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# One pool worker: drains the queue with its own browser agent + pacing
|
|
496
|
+
# until the poison pill (nil). parse_page swallows per-document errors, so
|
|
497
|
+
# the thread runs to the pill; the agent is quit by #process_hits.
|
|
498
|
+
def spawn_worker(queue, worker_agent)
|
|
499
|
+
pacing = Pacing.new
|
|
500
|
+
Thread.new do
|
|
501
|
+
while (item = queue.pop)
|
|
502
|
+
hit, pos = item
|
|
503
|
+
parse_page hit, pos, worker_agent, pacing
|
|
504
|
+
end
|
|
330
505
|
end
|
|
331
506
|
end
|
|
332
507
|
|
|
508
|
+
# Pacing for the serial listing phase (Phase 1). Also the fallback pacing
|
|
509
|
+
# for #time_req when no per-worker pacing is passed (direct calls / specs).
|
|
510
|
+
def listing_pacing
|
|
511
|
+
@listing_pacing ||= Pacing.new
|
|
512
|
+
end
|
|
513
|
+
|
|
333
514
|
RETRIABLE_ERRORS = [
|
|
334
515
|
SocketError,
|
|
335
516
|
Ferrum::TimeoutError,
|
|
336
517
|
Ferrum::PendingConnectionsError,
|
|
337
|
-
Ferrum::StatusError
|
|
518
|
+
Ferrum::StatusError,
|
|
519
|
+
BrowserAgent::ChallengeError
|
|
338
520
|
].freeze
|
|
339
521
|
|
|
340
|
-
|
|
522
|
+
# Run a throttled request through the given worker's pacing, retrying up to
|
|
523
|
+
# 4 times on a retriable error (challenge / Ferrum / socket) and backing
|
|
524
|
+
# that worker's gap off before each retry.
|
|
525
|
+
def time_req(pacing = listing_pacing)
|
|
526
|
+
pacing ||= listing_pacing
|
|
341
527
|
tries = 0
|
|
342
528
|
begin
|
|
343
529
|
tries += 1
|
|
344
|
-
|
|
530
|
+
pacing.throttle
|
|
345
531
|
yield
|
|
346
532
|
rescue *RETRIABLE_ERRORS => e
|
|
533
|
+
pacing.backoff
|
|
347
534
|
retry if tries < 4
|
|
348
535
|
raise e
|
|
349
|
-
ensure
|
|
350
|
-
@last_request_time = Time.now
|
|
351
536
|
end
|
|
352
537
|
end
|
|
353
538
|
end
|
|
@@ -59,7 +59,9 @@ module Relaton
|
|
|
59
59
|
#
|
|
60
60
|
def remove_index_file
|
|
61
61
|
require_relative "../cie"
|
|
62
|
-
Relaton::Index.find_or_create(
|
|
62
|
+
Relaton::Index.find_or_create(
|
|
63
|
+
:cie, url: true, file: "#{INDEXFILE}.yaml"
|
|
64
|
+
).remove_file
|
|
63
65
|
end
|
|
64
66
|
end
|
|
65
67
|
end
|
data/lib/relaton/cie/scrapper.rb
CHANGED
|
@@ -9,8 +9,21 @@ module Relaton
|
|
|
9
9
|
# @param code [String]
|
|
10
10
|
# @return [Relaton::Cie::ItemData]
|
|
11
11
|
def scrape_page(code)
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# An unrecognized reference raises; like ISO and 3GPP we let it
|
|
13
|
+
# propagate -- relaton-cli rescues Pubid::Errors::Error and renders
|
|
14
|
+
# "... is not a recognized standards identifier". Partial refs
|
|
15
|
+
# (`CIE 001`, `CIE 15`) parse, so nothing valid is lost.
|
|
16
|
+
pubid = ::Pubid::Cie.parse code
|
|
17
|
+
index = Index.find_or_create :cie, url: "#{ENDPOINT}#{INDEXFILE}.zip", file: "#{INDEXFILE}.yaml",
|
|
18
|
+
pubid_class: ::Pubid::Cie::Identifier
|
|
19
|
+
# Pass the parsed pubid (not the raw String) so index-v2 narrows
|
|
20
|
+
# candidates by number via binary search before the block runs; the
|
|
21
|
+
# block keeps the broad substring match the string index gave.
|
|
22
|
+
# Rows are Pubid::Cie::Identifier objects (not Comparable), so pick by
|
|
23
|
+
# the string form.
|
|
24
|
+
needle = pubid.to_s
|
|
25
|
+
row = index.search(pubid) { |r| r[:id].to_s.include?(needle) }
|
|
26
|
+
.min_by { |r| r[:id].to_s }
|
|
14
27
|
return unless row
|
|
15
28
|
|
|
16
29
|
parse_page "#{ENDPOINT}#{row[:file]}", code
|
data/lib/relaton/cie.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "nokogiri"
|
|
2
2
|
# require "parslet"
|
|
3
|
+
require "pubid"
|
|
3
4
|
require "relaton/index"
|
|
4
5
|
require "relaton/bib"
|
|
5
6
|
# require "relaton_bib/name_parser"
|
|
@@ -14,7 +15,7 @@ require_relative "cie/scrapper"
|
|
|
14
15
|
|
|
15
16
|
module Relaton
|
|
16
17
|
module Cie
|
|
17
|
-
INDEXFILE = "index-
|
|
18
|
+
INDEXFILE = "index-v2".freeze
|
|
18
19
|
|
|
19
20
|
# Returns hash of XML reammar
|
|
20
21
|
# @return [String]
|
|
@@ -15,6 +15,16 @@ module Relaton
|
|
|
15
15
|
@format = format
|
|
16
16
|
@ext = format.sub "bibxml", "xml"
|
|
17
17
|
@files = Set.new
|
|
18
|
+
# path => docid that reserved it, for #unique_output_file. Distinct
|
|
19
|
+
# from @files, which flavors use for their own duplicate handling:
|
|
20
|
+
# this one has to know WHICH document owns a path, not just that it
|
|
21
|
+
# is taken.
|
|
22
|
+
@file_docids = {}
|
|
23
|
+
# Paths this process wrote during this run; see #write_unique.
|
|
24
|
+
@written = Set.new
|
|
25
|
+
# Set true by a fetcher that writes from forked worker processes,
|
|
26
|
+
# where @file_docids cannot see a peer's claim. See #write_unique.
|
|
27
|
+
@cross_process = false
|
|
18
28
|
# @docs = []
|
|
19
29
|
@errors = Hash.new(true)
|
|
20
30
|
end
|
|
@@ -55,7 +65,14 @@ module Relaton
|
|
|
55
65
|
|
|
56
66
|
def report_errors
|
|
57
67
|
gh_issue # register the channel before logging
|
|
58
|
-
@errors.select { |_, v| v }.
|
|
68
|
+
@errors.select { |_, v| v }.each do |key, value|
|
|
69
|
+
# A String value IS the message: a specific, per-document failure
|
|
70
|
+
# such as an unparseable identifier, reported through this same
|
|
71
|
+
# channel without needing a per-flavor override. A boolean means
|
|
72
|
+
# "this field failed for every record" — what the flavors' own
|
|
73
|
+
# ERROR_KEYS track — and its message is derived from the key.
|
|
74
|
+
log_error value.is_a?(String) ? value : "Failed to fetch #{key}"
|
|
75
|
+
end
|
|
59
76
|
@gh_issue&.create_issue
|
|
60
77
|
end
|
|
61
78
|
|
|
@@ -63,11 +80,111 @@ module Relaton
|
|
|
63
80
|
raise NoMatchingPatternError, "#{self.class}#log_error method must be implemented"
|
|
64
81
|
end
|
|
65
82
|
|
|
83
|
+
# Most filesystems cap a single path component at 255 bytes.
|
|
84
|
+
MAX_BASENAME_BYTES = 255
|
|
85
|
+
|
|
86
|
+
# Create-or-fail. The failure is the point: it is how one process learns
|
|
87
|
+
# that another already holds a path. See #write_unique.
|
|
88
|
+
EXCLUSIVE = File::WRONLY | File::CREAT | File::EXCL
|
|
89
|
+
|
|
66
90
|
# @param [String] document ID
|
|
67
91
|
# @return [String] filename based on PubID identifier
|
|
92
|
+
#
|
|
93
|
+
# A docid can be pathologically long (pubid's `to_s` for amendment docs
|
|
94
|
+
# embeds the full "(Amendment to … as amended by …)" clause), which would
|
|
95
|
+
# make the basename exceed the OS limit and raise Errno::ENAMETOOLONG on
|
|
96
|
+
# write. When that happens, truncate the sanitized id and append a short
|
|
97
|
+
# digest of the full docid so the filename stays bounded, unique, and
|
|
98
|
+
# deterministic (every call site round-trips through this method).
|
|
68
99
|
def output_file(docid)
|
|
69
|
-
id = docid.downcase.gsub(/[
|
|
70
|
-
|
|
100
|
+
id = docid.downcase.gsub(/[.,\s\/:()-]+/, "-").delete_suffix("-")
|
|
101
|
+
ext = ".#{@ext}"
|
|
102
|
+
limit = MAX_BASENAME_BYTES - ext.bytesize
|
|
103
|
+
if id.bytesize > limit
|
|
104
|
+
require "digest"
|
|
105
|
+
suffix = "-#{Digest::SHA1.hexdigest(docid)[0, 12]}"
|
|
106
|
+
# `id` has no consecutive "-" (gsub collapsed runs above), so
|
|
107
|
+
# truncation leaves at most one trailing "-" — delete_suffix is
|
|
108
|
+
# enough and avoids a polynomial-ReDoS regex on the docid.
|
|
109
|
+
id = id.byteslice(0, limit - suffix.bytesize).scrub("").delete_suffix("-") + suffix
|
|
110
|
+
end
|
|
111
|
+
File.join @output, "#{id}#{ext}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
#
|
|
115
|
+
# Reserve a unique output path for `docid`.
|
|
116
|
+
#
|
|
117
|
+
# `output_file` sanitizes ".", ",", "/", ":", "(", ")", "-" and whitespace
|
|
118
|
+
# all to "-", so two DISTINCT docids can map to one path — live instance in
|
|
119
|
+
# relaton-data-iana: `rpki/signed-objects` and `rpki-signed-objects` both
|
|
120
|
+
# give `data/rpki-signed-objects.yaml`. A caller that merely warns and
|
|
121
|
+
# writes anyway leaves one file holding the wrong document for one of two
|
|
122
|
+
# index ids: a wrong answer, not a missing one.
|
|
123
|
+
#
|
|
124
|
+
# Returns `output_file(docid)` when that path is free, or when it is
|
|
125
|
+
# already held by this SAME docid — a genuine duplicate is the caller's
|
|
126
|
+
# business (skip / merge / last-wins), and this method must not turn one
|
|
127
|
+
# into two files. Only a real clash with a DIFFERENT docid gets a variant,
|
|
128
|
+
# suffixed with a digest of this docid so the name depends on the document
|
|
129
|
+
# and not on encounter order: adding a record never renames an existing
|
|
130
|
+
# file. (Which member of a clashing pair keeps the plain path does follow
|
|
131
|
+
# write order, which is stable for a given corpus.)
|
|
132
|
+
#
|
|
133
|
+
# @param [String] docid
|
|
134
|
+
# @return [String] path, reserved for this docid
|
|
135
|
+
#
|
|
136
|
+
def unique_output_file(docid)
|
|
137
|
+
file = output_file docid
|
|
138
|
+
owner = @file_docids[file]
|
|
139
|
+
file = digest_output_file(docid) unless owner.nil? || owner == docid
|
|
140
|
+
reserve file, docid
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
#
|
|
144
|
+
# Write `content` for `docid`, never clobbering a different document.
|
|
145
|
+
#
|
|
146
|
+
# `output_file` is not injective, so the plain path may belong to someone
|
|
147
|
+
# else. `unique_output_file` settles that within this process; the file is
|
|
148
|
+
# then created with `O_EXCL` so a peer *process* cannot be clobbered
|
|
149
|
+
# either. Returns the path actually written.
|
|
150
|
+
#
|
|
151
|
+
# The four outcomes, each load-bearing:
|
|
152
|
+
#
|
|
153
|
+
# * **We already wrote this path for this docid** — plain overwrite, so a
|
|
154
|
+
# genuine duplicate stays one file and the flavor's own duplicate
|
|
155
|
+
# handling (skip / merge / last-wins) still decides what happens.
|
|
156
|
+
# * **`O_EXCL` succeeds** — the normal case.
|
|
157
|
+
# * **`EEXIST`, single process** — `unique_output_file` has already proved
|
|
158
|
+
# that no peer of ours holds this path, so the file can only be a
|
|
159
|
+
# leftover from an earlier crawl. Overwrite it. Every single-process
|
|
160
|
+
# flavor takes this branch, and it involves no guesswork.
|
|
161
|
+
# * **`EEXIST`, `@cross_process`** — a peer's file and a leftover are
|
|
162
|
+
# indistinguishable, so take a path of our own rather than risk
|
|
163
|
+
# destroying a record. The caller reconciles the names afterwards, in
|
|
164
|
+
# the parent, where it can see every docid (see
|
|
165
|
+
# `Relaton::Ietf::DataFetcher#reconcile_output_files`).
|
|
166
|
+
#
|
|
167
|
+
# Deliberately NOT a wall-clock "is this file older than the crawl?" test.
|
|
168
|
+
# A crawl runs into a populated `data/`, so `EEXIST` is the common case on
|
|
169
|
+
# a re-run and such a test would decide it for every record; its
|
|
170
|
+
# false-"stale" direction is the silent overwrite this method exists to
|
|
171
|
+
# prevent, and coarse filesystem mtime granularity makes that reachable.
|
|
172
|
+
#
|
|
173
|
+
# @param [String] docid
|
|
174
|
+
# @param [String] content serialized document
|
|
175
|
+
# @return [String] path written
|
|
176
|
+
#
|
|
177
|
+
def write_unique(docid, content)
|
|
178
|
+
file = unique_output_file docid
|
|
179
|
+
return force_write(file, content) if @written.include?(file)
|
|
180
|
+
|
|
181
|
+
File.write file, content, mode: EXCLUSIVE, encoding: "UTF-8"
|
|
182
|
+
@written << file
|
|
183
|
+
file
|
|
184
|
+
rescue Errno::EEXIST
|
|
185
|
+
return force_write(file, content) unless @cross_process
|
|
186
|
+
|
|
187
|
+
force_write reserve(digest_output_file(docid), docid), content
|
|
71
188
|
end
|
|
72
189
|
|
|
73
190
|
#
|
|
@@ -92,6 +209,36 @@ module Relaton
|
|
|
92
209
|
def to_bibxml(bib)
|
|
93
210
|
raise NotImplementedError, "#{self.class}#to_bibxml method must be implemented"
|
|
94
211
|
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
# Write, clobbering whatever is there. The caller has already established
|
|
216
|
+
# that the path is ours to take.
|
|
217
|
+
def force_write(file, content)
|
|
218
|
+
File.write file, content, encoding: "UTF-8"
|
|
219
|
+
@written << file
|
|
220
|
+
file
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Record `docid` as the owner of `file`, unless someone got there first,
|
|
224
|
+
# and return `file`. The reservation table must never hand one path to two
|
|
225
|
+
# documents; see #unique_output_file.
|
|
226
|
+
def reserve(file, docid)
|
|
227
|
+
@file_docids[file] ||= docid
|
|
228
|
+
file
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# `output_file`'s path with a docid digest appended to the basename, kept
|
|
232
|
+
# inside the same byte cap. Deterministic: same docid, same name.
|
|
233
|
+
def digest_output_file(docid)
|
|
234
|
+
require "digest"
|
|
235
|
+
ext = ".#{@ext}"
|
|
236
|
+
suffix = "-#{Digest::SHA1.hexdigest(docid)[0, 12]}"
|
|
237
|
+
stem = File.basename output_file(docid), ext
|
|
238
|
+
limit = MAX_BASENAME_BYTES - ext.bytesize - suffix.bytesize
|
|
239
|
+
stem = stem.byteslice(0, limit).scrub("").delete_suffix("-") if stem.bytesize > limit
|
|
240
|
+
File.join @output, "#{stem}#{suffix}#{ext}"
|
|
241
|
+
end
|
|
95
242
|
end
|
|
96
243
|
end
|
|
97
244
|
end
|