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.
@@ -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 either an existing index or a path to create one
237
- if index_or_path.is_a?(MultistreamIndex)
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
- idx = @index.stream_offsets.index(current_offset)
369
- return nil unless idx
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
- @index.stream_offsets[idx + 1]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wp2txt
4
- VERSION = "2.1.2"
4
+ VERSION = "2.3.0"
5
5
  end
@@ -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