wp2txt 2.1.2 → 2.3.0
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/.gitignore +3 -0
- data/CHANGELOG.md +27 -0
- data/Dockerfile +3 -0
- data/Gemfile +2 -0
- data/README.md +60 -2
- data/Rakefile +4 -1
- data/bin/wp2txt +9 -0
- data/bin/wp2txt-mcp +395 -0
- data/docs/RESEARCH.md +207 -0
- data/lib/wp2txt/cli.rb +110 -0
- data/lib/wp2txt/corpus.rb +1057 -0
- data/lib/wp2txt/corpus_jobs.rb +106 -0
- data/lib/wp2txt/fts_index.rb +445 -0
- data/lib/wp2txt/index_cache.rb +17 -0
- data/lib/wp2txt/index_commands.rb +427 -0
- data/lib/wp2txt/langlinks_importer.rb +273 -0
- data/lib/wp2txt/metadata_index.rb +738 -0
- data/lib/wp2txt/multistream.rb +38 -5
- data/lib/wp2txt/output_path.rb +27 -0
- data/lib/wp2txt/version.rb +1 -1
- data/spec/auto_download_spec.rb +77 -0
- data/spec/corpus_spec.rb +503 -0
- data/spec/fts_index_spec.rb +245 -0
- data/spec/langlinks_importer_spec.rb +308 -0
- data/spec/metadata_index_spec.rb +208 -0
- data/spec/multi_dump_attach_spec.rb +174 -0
- data/spec/support/meta_db_fixture.rb +53 -0
- data/spec/support/multistream_fixture.rb +66 -0
- data/spec/titles_output_path_spec.rb +338 -0
- metadata +27 -1
data/lib/wp2txt/multistream.rb
CHANGED
|
@@ -233,8 +233,9 @@ module Wp2txt
|
|
|
233
233
|
def initialize(multistream_path, index_or_path, use_cache: true, cache_dir: nil)
|
|
234
234
|
@multistream_path = multistream_path
|
|
235
235
|
|
|
236
|
-
# Accept
|
|
237
|
-
|
|
236
|
+
# Accept an existing index (or any object with the same lookup interface,
|
|
237
|
+
# e.g. a lazy SQLite-backed one) or a path to create one
|
|
238
|
+
if index_or_path.respond_to?(:find_by_title)
|
|
238
239
|
@index = index_or_path
|
|
239
240
|
else
|
|
240
241
|
@index = MultistreamIndex.new(index_or_path, use_cache: use_cache, cache_dir: cache_dir)
|
|
@@ -365,10 +366,13 @@ module Wp2txt
|
|
|
365
366
|
end
|
|
366
367
|
|
|
367
368
|
def find_next_offset(current_offset)
|
|
368
|
-
|
|
369
|
-
|
|
369
|
+
offsets = @index.stream_offsets
|
|
370
|
+
idx = offsets.index(current_offset)
|
|
371
|
+
# A missing offset means a corrupt or empty stream index; returning nil
|
|
372
|
+
# here would silently read gigabytes to EOF, so fail fast instead
|
|
373
|
+
raise "Stream offset #{current_offset} not found in index (#{offsets.size} streams known)" unless idx
|
|
370
374
|
|
|
371
|
-
|
|
375
|
+
offsets[idx + 1]
|
|
372
376
|
end
|
|
373
377
|
|
|
374
378
|
def decompress_bz2(data)
|
|
@@ -822,6 +826,35 @@ module Wp2txt
|
|
|
822
826
|
File.join(@cache_dir, "#{@lang}wiki-#{latest_dump_date}-multistream.xml.bz2")
|
|
823
827
|
end
|
|
824
828
|
|
|
829
|
+
# URL of the langlinks dump for an explicit dump date
|
|
830
|
+
def langlinks_url(date)
|
|
831
|
+
wiki = "#{@lang}wiki"
|
|
832
|
+
"#{DUMP_BASE_URL}/#{wiki}/#{date}/#{wiki}-#{date}-langlinks.sql.gz"
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# Cache path of the langlinks dump for an explicit dump date
|
|
836
|
+
def cached_langlinks_path(date)
|
|
837
|
+
File.join(@cache_dir, "#{@lang}wiki-#{date}-langlinks.sql.gz")
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
# Download the langlinks dump for an explicit dump date. The date MUST
|
|
841
|
+
# come from the built metadata index (not latest_dump_date) so the
|
|
842
|
+
# imported links stay pinned to the indexed dump version.
|
|
843
|
+
def download_langlinks(date:, force: false)
|
|
844
|
+
path = cached_langlinks_path(date)
|
|
845
|
+
if File.exist?(path) && !force
|
|
846
|
+
puts "Langlinks already cached: #{File.basename(path)}"
|
|
847
|
+
$stdout.flush
|
|
848
|
+
return path
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
url = langlinks_url(date)
|
|
852
|
+
puts "Downloading langlinks: #{url}"
|
|
853
|
+
$stdout.flush
|
|
854
|
+
download_file(url, path)
|
|
855
|
+
path
|
|
856
|
+
end
|
|
857
|
+
|
|
825
858
|
# Check if cache is fresh (within configured days)
|
|
826
859
|
def cache_fresh?(days = nil)
|
|
827
860
|
days ||= @dump_expiry_days
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wp2txt
|
|
4
|
+
# Server-side output path confinement shared by the file-writing tools
|
|
5
|
+
# (extract_corpus / start_extract_job / query_sql with output_path).
|
|
6
|
+
# Paths must resolve under the server's output directory — an agent mixing
|
|
7
|
+
# up paths must not be able to clobber arbitrary user files — and existing
|
|
8
|
+
# files are not replaced unless overwrite is set.
|
|
9
|
+
module OutputPath
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# @return [String] the confined, absolute output path
|
|
13
|
+
# @raise [ArgumentError] when the path escapes base_dir or the file exists
|
|
14
|
+
def confine(output_path, base_dir, overwrite: false)
|
|
15
|
+
path = File.expand_path(output_path, base_dir)
|
|
16
|
+
base = File.expand_path(base_dir)
|
|
17
|
+
unless path == base || path.start_with?(base + File::SEPARATOR)
|
|
18
|
+
raise ArgumentError, "output_path must stay within the server output directory (#{base})"
|
|
19
|
+
end
|
|
20
|
+
if File.exist?(path) && !overwrite
|
|
21
|
+
raise ArgumentError, "output file already exists: #{path} (pass overwrite: true to replace it)"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
path
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/wp2txt/version.rb
CHANGED
data/spec/auto_download_spec.rb
CHANGED
|
@@ -33,6 +33,26 @@ RSpec.describe "Wp2txt Auto Download" do
|
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
describe "langlinks paths" do
|
|
37
|
+
it "builds URL and cache path for an explicit dump date (no network)" do
|
|
38
|
+
manager = Wp2txt::DumpManager.new(:ja, cache_dir: cache_dir)
|
|
39
|
+
expect(manager.langlinks_url("20260101")).to eq(
|
|
40
|
+
"https://dumps.wikimedia.org/jawiki/20260101/jawiki-20260101-langlinks.sql.gz"
|
|
41
|
+
)
|
|
42
|
+
expect(manager.cached_langlinks_path("20260101")).to eq(
|
|
43
|
+
File.join(cache_dir, "jawiki-20260101-langlinks.sql.gz")
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "reuses a cached langlinks file without downloading" do
|
|
48
|
+
manager = Wp2txt::DumpManager.new(:ja, cache_dir: cache_dir)
|
|
49
|
+
path = manager.cached_langlinks_path("20260101")
|
|
50
|
+
File.write(path, "cached")
|
|
51
|
+
expect(manager.download_langlinks(date: "20260101")).to eq(path)
|
|
52
|
+
expect(File.read(path)).to eq("cached")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
36
56
|
describe "#cache_status" do
|
|
37
57
|
it "returns status hash with expected keys" do
|
|
38
58
|
manager = Wp2txt::DumpManager.new(:ja, cache_dir: cache_dir)
|
|
@@ -285,6 +305,63 @@ RSpec.describe "Wp2txt Auto Download" do
|
|
|
285
305
|
end
|
|
286
306
|
end
|
|
287
307
|
end
|
|
308
|
+
|
|
309
|
+
context "--import-langlinks option" do
|
|
310
|
+
it "accepts --import-langlinks with --lang" do
|
|
311
|
+
opts = Wp2txt::CLI.parse_options(["--lang=ja", "--import-langlinks", "--cache-dir=#{cache_dir}"])
|
|
312
|
+
expect(opts[:import_langlinks]).to be true
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it "requires --lang" do
|
|
316
|
+
suppress_stderr do
|
|
317
|
+
expect { Wp2txt::CLI.parse_options(["--import-langlinks", "--cache-dir=#{cache_dir}"]) }.to raise_error(SystemExit)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
it "cannot be combined with --build-index" do
|
|
322
|
+
suppress_stderr do
|
|
323
|
+
expect do
|
|
324
|
+
Wp2txt::CLI.parse_options(["--lang=ja", "--import-langlinks", "--build-index", "--cache-dir=#{cache_dir}"])
|
|
325
|
+
end.to raise_error(SystemExit)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
it "accepts --langlinks-file with an existing file" do
|
|
330
|
+
file = File.join(cache_dir, "jawiki-20260101-langlinks.sql")
|
|
331
|
+
File.write(file, "")
|
|
332
|
+
opts = Wp2txt::CLI.parse_options(["--lang=ja", "--import-langlinks", "--langlinks-file=#{file}", "--cache-dir=#{cache_dir}"])
|
|
333
|
+
expect(opts[:langlinks_file]).to eq(file)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
it "rejects --langlinks-file without --import-langlinks" do
|
|
337
|
+
suppress_stderr do
|
|
338
|
+
expect do
|
|
339
|
+
Wp2txt::CLI.parse_options(["--lang=ja", "--langlinks-file=x.sql", "--cache-dir=#{cache_dir}"])
|
|
340
|
+
end.to raise_error(SystemExit)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
it "rejects a missing --langlinks-file" do
|
|
345
|
+
suppress_stderr do
|
|
346
|
+
expect do
|
|
347
|
+
Wp2txt::CLI.parse_options(["--lang=ja", "--import-langlinks", "--langlinks-file=#{cache_dir}/nope.sql", "--cache-dir=#{cache_dir}"])
|
|
348
|
+
end.to raise_error(SystemExit)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it "accepts --langlinks-langs as a comma-separated list" do
|
|
353
|
+
opts = Wp2txt::CLI.parse_options(["--lang=ja", "--import-langlinks", "--langlinks-langs=en,de,fr", "--cache-dir=#{cache_dir}"])
|
|
354
|
+
expect(opts[:langlinks_langs]).to eq("en,de,fr")
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
it "rejects invalid language codes in --langlinks-langs" do
|
|
358
|
+
suppress_stderr do
|
|
359
|
+
expect do
|
|
360
|
+
Wp2txt::CLI.parse_options(["--lang=ja", "--import-langlinks", "--langlinks-langs=en,../x", "--cache-dir=#{cache_dir}"])
|
|
361
|
+
end.to raise_error(SystemExit)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
288
365
|
end
|
|
289
366
|
end
|
|
290
367
|
|