e621_export_downloader 0.0.17 → 0.0.18
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/CHANGELOG.md +4 -0
- data/README.md +6 -4
- data/Rakefile +16 -6
- data/lib/e621/tag_alias.rb +1 -1
- data/lib/e621/tag_implication.rb +1 -1
- data/lib/e621_export_downloader/api_export_data.rb +2 -1
- data/lib/e621_export_downloader/client/options/builder.rb +6 -2
- data/lib/e621_export_downloader/client/options.rb +1 -0
- data/lib/e621_export_downloader/client.rb +5 -2
- data/lib/e621_export_downloader/export.rb +10 -0
- data/lib/e621_export_downloader/models/tag_alias.rb +4 -4
- data/lib/e621_export_downloader/models/tag_implication.rb +4 -4
- data/lib/e621_export_downloader/version.rb +1 -1
- data/lib/e621_export_downloader.rb +7 -0
- data/lib/generators/e621_export_downloader/templates/migrations/create_e621_tables.rb.erb +3 -1
- data/test/export_test.rb +93 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7259f8077bf1df8805620f4efa3fbeac43c29616e496bea576518275794c5fef
|
|
4
|
+
data.tar.gz: 4b23216298e535aeed4ceaeb7f38f581bf6fc6fb68c1f7fa94975e9157f7bf71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50436457d2de9e91e8f938631484653a71d8102122ec58f77c8c6287454092c38ef6719215a4aa20cb676978d659ce936750338667d892045027b4ee0a0fa0ed
|
|
7
|
+
data.tar.gz: b8a0c19646cd5b7ed2076b023adabc016962700dcc4c755ef2b66d3f3b018349be45aafca1c4be1dc8c0be9c8f2dd6de5f1529f6ea27f45a04df5660d6f6f1a9
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -23,12 +23,13 @@ client = E621ExportDownloader::Client.new
|
|
|
23
23
|
|
|
24
24
|
# configure options after creation
|
|
25
25
|
client.config do |c|
|
|
26
|
-
c.cache = true
|
|
26
|
+
c.cache = true # keep export files after reading, defaults to false
|
|
27
|
+
c.validate_checksum = true # verify the downloaded file against the API-provided checksum, defaults to false
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
# or pass an Options struct directly
|
|
30
31
|
client = E621ExportDownloader::Client.new(
|
|
31
|
-
E621ExportDownloader::Client::Options.new(cache: true)
|
|
32
|
+
E621ExportDownloader::Client::Options.new(cache: true, validate_checksum: true)
|
|
32
33
|
)
|
|
33
34
|
|
|
34
35
|
# get the export for a type — resolves the latest available export from the e621 API
|
|
@@ -39,10 +40,10 @@ export = client.get("posts")
|
|
|
39
40
|
deferred = client.get_deferred("posts")
|
|
40
41
|
|
|
41
42
|
# get raw metadata for all exports from the e621 API
|
|
42
|
-
# returns an Array of E621ExportDownloader::APIExportData: { file_name, file_size, name, updated_at, url }
|
|
43
|
+
# returns an Array of E621ExportDownloader::APIExportData: { file_name, file_size, name, checksum, updated_at, url }
|
|
43
44
|
data = client.get_data
|
|
44
45
|
|
|
45
|
-
# access the metadata for this specific export: { file_name, file_size, name, updated_at, url }
|
|
46
|
+
# access the metadata for this specific export: { file_name, file_size, name, checksum, updated_at, url }
|
|
46
47
|
export.data
|
|
47
48
|
|
|
48
49
|
# check if the export exists
|
|
@@ -50,6 +51,7 @@ exists = export.exists?
|
|
|
50
51
|
|
|
51
52
|
# download the export, returns the file path — not required before reading
|
|
52
53
|
# if you move or remove the file do not reuse the export object
|
|
54
|
+
# raises ResolveError if validate_checksum is enabled and the downloaded file's checksum does not match the API
|
|
53
55
|
file = export.download
|
|
54
56
|
|
|
55
57
|
# delete the downloaded file, if it exists
|
data/Rakefile
CHANGED
|
@@ -8,11 +8,21 @@ Rake::TestTask.new(:test) do |t|
|
|
|
8
8
|
t.pattern = "test/**/*_test.rb"
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
exec(%(bundle exec ruby -e "require 'e621_export_downloader'; Zeitwerk::Loader.eager_load_all"))
|
|
15
|
-
end
|
|
11
|
+
desc("Test Zeitwerk Auto-Importing")
|
|
12
|
+
task(:zeitwerk) do
|
|
13
|
+
ruby(%(-e "require 'e621_export_downloader'; Zeitwerk::Loader.eager_load_all"))
|
|
16
14
|
end
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
desc("Run all tests")
|
|
17
|
+
task(:all) do
|
|
18
|
+
puts("=== rubocop ===")
|
|
19
|
+
sh("bundle exec rubocop")
|
|
20
|
+
puts("=== zeitwerk ===")
|
|
21
|
+
sh("bundle exec rake zeitwerk")
|
|
22
|
+
puts("=== test ===")
|
|
23
|
+
sh("bundle exec rake test")
|
|
24
|
+
puts("=== srb ===")
|
|
25
|
+
sh("bundle exec srb tc")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
task(default: :all)
|
data/lib/e621/tag_alias.rb
CHANGED
|
@@ -29,7 +29,7 @@ module E621
|
|
|
29
29
|
approver_id: record.approver_id,
|
|
30
30
|
consequent_name: record.consequent_name,
|
|
31
31
|
created_at: record.created_at,
|
|
32
|
-
|
|
32
|
+
creator_id: record.creator_id,
|
|
33
33
|
down_votes: record.down_votes,
|
|
34
34
|
forum_post_id: record.forum_post_id,
|
|
35
35
|
forum_topic_id: record.forum_topic_id,
|
data/lib/e621/tag_implication.rb
CHANGED
|
@@ -29,7 +29,7 @@ module E621
|
|
|
29
29
|
approver_id: record.approver_id,
|
|
30
30
|
consequent_name: record.consequent_name,
|
|
31
31
|
created_at: record.created_at,
|
|
32
|
-
|
|
32
|
+
creator_id: record.creator_id,
|
|
33
33
|
descendant_names: record.descendant_names,
|
|
34
34
|
down_votes: record.down_votes,
|
|
35
35
|
forum_post_id: record.forum_post_id,
|
|
@@ -10,13 +10,17 @@ module E621ExportDownloader
|
|
|
10
10
|
sig { returns(T::Boolean) }
|
|
11
11
|
attr_accessor(:cache)
|
|
12
12
|
|
|
13
|
+
sig { returns(T::Boolean) }
|
|
14
|
+
attr_accessor(:validate_checksum)
|
|
15
|
+
|
|
13
16
|
sig { params(parsers: Options::Parsers).returns(Options::Parsers) }
|
|
14
17
|
attr_writer(:parsers)
|
|
15
18
|
|
|
16
19
|
sig { params(defaults: Options).void }
|
|
17
20
|
def initialize(defaults = Options.new)
|
|
18
|
-
@cache
|
|
19
|
-
@
|
|
21
|
+
@cache = T.let(defaults.cache, T::Boolean)
|
|
22
|
+
@validate_checksum = T.let(defaults.validate_checksum, T::Boolean)
|
|
23
|
+
@parsers = T.let(defaults.parsers, Options::Parsers)
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
sig { params(block: T.nilable(T.proc.params(arg0: Builder::Parsers).void)).returns(Options::Parsers) }
|
|
@@ -21,7 +21,9 @@ module E621ExportDownloader
|
|
|
21
21
|
|
|
22
22
|
sig { params(block: T.proc.params(arg0: Options::Builder).void).void }
|
|
23
23
|
def config(&block)
|
|
24
|
-
|
|
24
|
+
builder = Options::Builder.new(@options)
|
|
25
|
+
block.call(builder)
|
|
26
|
+
@options = Options.new(cache: builder.cache, validate_checksum: builder.validate_checksum, parsers: builder.parsers)
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
sig { returns(Faraday::Connection) }
|
|
@@ -61,9 +63,10 @@ module E621ExportDownloader
|
|
|
61
63
|
data = JSON.parse(T.unsafe(res).body)
|
|
62
64
|
result = T.let(data.map do |d|
|
|
63
65
|
APIExportData.new(
|
|
66
|
+
name: Types.deserialize(d["name"]),
|
|
64
67
|
file_name: d["file_name"],
|
|
65
68
|
file_size: d["file_size"].to_i,
|
|
66
|
-
|
|
69
|
+
checksum: d["checksum"],
|
|
67
70
|
updated_at: d["updated_at"],
|
|
68
71
|
url: d["url"],
|
|
69
72
|
)
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
# typed: strict
|
|
3
3
|
|
|
4
4
|
require("csv")
|
|
5
|
+
require("date")
|
|
6
|
+
require("digest")
|
|
7
|
+
require("fileutils")
|
|
8
|
+
require("zlib")
|
|
5
9
|
|
|
6
10
|
module E621ExportDownloader
|
|
7
11
|
class Export
|
|
@@ -51,11 +55,13 @@ module E621ExportDownloader
|
|
|
51
55
|
client.debug("downloading export", header: ["export:#{type.serialize}"])
|
|
52
56
|
|
|
53
57
|
FileUtils.mkdir_p(Constants::TEMP_DIR)
|
|
58
|
+
digest = T.let(client.options.validate_checksum ? Digest::MD5.new : nil, T.nilable(Digest::MD5))
|
|
54
59
|
File.open(file_path, "wb") do |file|
|
|
55
60
|
inflater = Zlib::Inflate.new(Zlib::MAX_WBITS + 16)
|
|
56
61
|
|
|
57
62
|
res = client.connection.get(data.url) do |req|
|
|
58
63
|
req.options.on_data = proc do |chunk, received|
|
|
64
|
+
digest&.update(chunk)
|
|
59
65
|
decompressed = inflater.inflate(chunk)
|
|
60
66
|
file.write(decompressed) if decompressed && !decompressed.empty?
|
|
61
67
|
block.call(received, data.file_size) if block
|
|
@@ -69,6 +75,10 @@ module E621ExportDownloader
|
|
|
69
75
|
unless res.success?
|
|
70
76
|
raise(ResolveError, "Failed to download export #{type.serialize}: #{res.status} #{res.reason_phrase}")
|
|
71
77
|
end
|
|
78
|
+
|
|
79
|
+
if digest && digest.hexdigest != data.checksum
|
|
80
|
+
raise(ResolveError, "Checksum mismatch for export #{type.serialize}")
|
|
81
|
+
end
|
|
72
82
|
rescue # rubocop:disable Style/RescueStandardError
|
|
73
83
|
FileUtils.rm_f(file_path)
|
|
74
84
|
raise
|
|
@@ -18,8 +18,8 @@ module E621ExportDownloader
|
|
|
18
18
|
sig { returns(T.nilable(DateTime)) }
|
|
19
19
|
attr_reader(:created_at)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
sig { returns(T.nilable(Integer)) }
|
|
22
|
+
attr_reader(:creator_id)
|
|
23
23
|
|
|
24
24
|
sig { returns(Integer) }
|
|
25
25
|
attr_reader(:down_votes)
|
|
@@ -58,7 +58,7 @@ module E621ExportDownloader
|
|
|
58
58
|
@approver_id = T.let(T.must(record["approver_id"]).empty? ? nil : record["approver_id"].to_i, T.nilable(Integer))
|
|
59
59
|
@consequent_name = T.let(T.must(record["consequent_name"]), String)
|
|
60
60
|
@created_at = T.let(T.must(record["created_at"]).empty? ? nil : DateTime.parse(record["created_at"]), T.nilable(DateTime))
|
|
61
|
-
|
|
61
|
+
@creator_id = T.let(T.must(record["creator_id"]).empty? ? nil : record["creator_id"].to_i, T.nilable(Integer))
|
|
62
62
|
@down_votes = T.let(record["down_votes"].to_i, Integer)
|
|
63
63
|
@forum_post_id = T.let(T.must(record["forum_post_id"]).empty? ? nil : record["forum_post_id"].to_i, T.nilable(Integer))
|
|
64
64
|
@forum_topic_id = T.let(T.must(record["forum_topic_id"]).empty? ? nil : record["forum_topic_id"].to_i, T.nilable(Integer))
|
|
@@ -78,7 +78,7 @@ module E621ExportDownloader
|
|
|
78
78
|
approver_id: @approver_id,
|
|
79
79
|
consequent_name: @consequent_name,
|
|
80
80
|
created_at: @created_at,
|
|
81
|
-
|
|
81
|
+
creator_id: @creator_id,
|
|
82
82
|
down_votes: @down_votes,
|
|
83
83
|
forum_post_id: @forum_post_id,
|
|
84
84
|
forum_topic_id: @forum_topic_id,
|
|
@@ -18,8 +18,8 @@ module E621ExportDownloader
|
|
|
18
18
|
sig { returns(T.nilable(DateTime)) }
|
|
19
19
|
attr_reader(:created_at)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
sig { returns(T.nilable(Integer)) }
|
|
22
|
+
attr_reader(:creator_id)
|
|
23
23
|
|
|
24
24
|
sig { returns(T::Array[String]) }
|
|
25
25
|
attr_reader(:descendant_names)
|
|
@@ -58,7 +58,7 @@ module E621ExportDownloader
|
|
|
58
58
|
@approver_id = T.let(T.must(record["approver_id"]).empty? ? nil : record["approver_id"].to_i, T.nilable(Integer))
|
|
59
59
|
@consequent_name = T.let(T.must(record["consequent_name"]), String)
|
|
60
60
|
@created_at = T.let(T.must(record["created_at"]).empty? ? nil : DateTime.parse(record["created_at"]), T.nilable(DateTime))
|
|
61
|
-
|
|
61
|
+
@creator_id = T.let(T.must(record["creator_id"]).empty? ? nil : record["creator_id"].to_i, T.nilable(Integer))
|
|
62
62
|
inner = T.must(T.must(record["descendant_names"])[1..-2])
|
|
63
63
|
@descendant_names = T.let(inner.empty? ? [] : inner.split(","), T::Array[String])
|
|
64
64
|
@down_votes = T.let(record["down_votes"].to_i, Integer)
|
|
@@ -79,7 +79,7 @@ module E621ExportDownloader
|
|
|
79
79
|
approver_id: @approver_id,
|
|
80
80
|
consequent_name: @consequent_name,
|
|
81
81
|
created_at: @created_at,
|
|
82
|
-
|
|
82
|
+
creator_id: @creator_id,
|
|
83
83
|
descendant_names: @descendant_names,
|
|
84
84
|
down_votes: @down_votes,
|
|
85
85
|
forum_post_id: @forum_post_id,
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
require("zeitwerk")
|
|
4
4
|
require("sorbet-runtime")
|
|
5
5
|
|
|
6
|
+
# Loaded eagerly (rather than left to Zeitwerk) because Bundler's gemspec evaluation already
|
|
7
|
+
# requires version.rb directly to read the gem version, which predefines E621ExportDownloader::Constants
|
|
8
|
+
# before Zeitwerk::Loader#setup runs below. That premature definition makes Zeitwerk skip autoloading
|
|
9
|
+
# constants.rb entirely, since it only autoloads namespaces it hasn't already seen defined.
|
|
10
|
+
require_relative("e621_export_downloader/constants")
|
|
11
|
+
|
|
6
12
|
loader = Zeitwerk::Loader.for_gem
|
|
7
13
|
loader.inflector.inflect({ "api_export_data" => "APIExportData" })
|
|
8
14
|
loader.ignore("#{__dir__}/e621_export_downloader/version.rb")
|
|
15
|
+
loader.ignore("#{__dir__}/e621_export_downloader/constants.rb")
|
|
9
16
|
loader.ignore("#{__dir__}/e621_export_downloader/railtie.rb")
|
|
10
17
|
loader.ignore("#{__dir__}/e621_export_downloader/serializers/active_job.rb")
|
|
11
18
|
loader.ignore("#{__dir__}/e621_export_downloader/active_record_models.rb")
|
|
@@ -68,7 +68,7 @@ class CreateE621Tables < ActiveRecord::Migration<%= migration_version %>
|
|
|
68
68
|
t.boolean(:is_pending, null: false)
|
|
69
69
|
t.boolean(:is_rating_locked, null: false)
|
|
70
70
|
t.boolean(:is_status_locked, null: false)
|
|
71
|
-
t.text(:locked_tags
|
|
71
|
+
t.text(:locked_tags)
|
|
72
72
|
t.string(:md5, null: false)
|
|
73
73
|
t.bigint(:parent_id)
|
|
74
74
|
t.string(:rating, null: false)
|
|
@@ -136,6 +136,7 @@ class CreateE621Tables < ActiveRecord::Migration<%= migration_version %>
|
|
|
136
136
|
t.integer(:down_votes, null: false)
|
|
137
137
|
t.integer(:meh_votes, null: false)
|
|
138
138
|
t.integer(:up_votes, null: false)
|
|
139
|
+
t.integer(:creator_id, null: false)
|
|
139
140
|
end
|
|
140
141
|
|
|
141
142
|
create_table(<%= table_ref("tag_implications") %>, id: :bigint, default: nil, force: :cascade) do |t|
|
|
@@ -152,6 +153,7 @@ class CreateE621Tables < ActiveRecord::Migration<%= migration_version %>
|
|
|
152
153
|
t.integer(:down_votes, null: false)
|
|
153
154
|
t.integer(:meh_votes, null: false)
|
|
154
155
|
t.integer(:up_votes, null: false)
|
|
156
|
+
t.integer(:creator_id, null: false)
|
|
155
157
|
end
|
|
156
158
|
|
|
157
159
|
create_table(<%= table_ref("tags") %>, id: :bigint, default: nil, force: :cascade) do |t|
|
data/test/export_test.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require("minitest/autorun")
|
|
4
|
+
require("digest")
|
|
5
|
+
require("fileutils")
|
|
6
|
+
require("zlib")
|
|
7
|
+
require("e621_export_downloader")
|
|
8
|
+
|
|
9
|
+
module ExportTestHelpers
|
|
10
|
+
# Stands in for a Faraday::Connection, driving the same #get(url) { |req| req.options.on_data = ... }
|
|
11
|
+
# streaming contract that Export#download relies on, without touching the network.
|
|
12
|
+
class FakeConnection
|
|
13
|
+
def initialize(chunks)
|
|
14
|
+
@chunks = chunks
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def get(_url)
|
|
18
|
+
req = Struct.new(:options).new(Struct.new(:on_data).new)
|
|
19
|
+
yield(req)
|
|
20
|
+
|
|
21
|
+
received = 0
|
|
22
|
+
@chunks.each do |chunk|
|
|
23
|
+
received += chunk.bytesize
|
|
24
|
+
req.options.on_data.call(chunk, received)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Struct.new(:status, :reason_phrase, :success?).new(200, "OK", true)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def head(_url)
|
|
31
|
+
Struct.new(:success?).new(true)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class ExportChecksumTest < Minitest::Test
|
|
37
|
+
include(ExportTestHelpers)
|
|
38
|
+
|
|
39
|
+
def setup
|
|
40
|
+
@csv_content = "id,name\n1,foo\n"
|
|
41
|
+
@compressed = Zlib.gzip(@csv_content)
|
|
42
|
+
@checksum = Digest::MD5.hexdigest(@compressed)
|
|
43
|
+
# split across chunks to exercise streaming, matching how a real download arrives
|
|
44
|
+
midpoint = @compressed.bytesize / 2
|
|
45
|
+
@chunks = [@compressed.byteslice(0, midpoint), @compressed.byteslice(midpoint..)]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def teardown
|
|
49
|
+
FileUtils.rm_rf(E621ExportDownloader::Constants::TEMP_DIR)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_export(checksum:, validate_checksum:)
|
|
53
|
+
data = E621ExportDownloader::APIExportData.new(
|
|
54
|
+
name: E621ExportDownloader::Types::Posts,
|
|
55
|
+
file_name: "posts-checksum-test.csv",
|
|
56
|
+
file_size: @compressed.bytesize,
|
|
57
|
+
checksum: checksum,
|
|
58
|
+
updated_at: "2026-01-01",
|
|
59
|
+
url: "https://example.com/posts.csv.gz",
|
|
60
|
+
)
|
|
61
|
+
client = E621ExportDownloader::Client.new(
|
|
62
|
+
E621ExportDownloader::Client::Options.new(validate_checksum: validate_checksum),
|
|
63
|
+
)
|
|
64
|
+
chunks = @chunks
|
|
65
|
+
client.define_singleton_method(:connection) { FakeConnection.new(chunks) }
|
|
66
|
+
E621ExportDownloader::Export.new(
|
|
67
|
+
data: data,
|
|
68
|
+
client: client,
|
|
69
|
+
type: E621ExportDownloader::Types::Posts,
|
|
70
|
+
parser: ->(record) { record },
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_download_succeeds_when_checksum_matches_and_validation_is_enabled
|
|
75
|
+
export = build_export(checksum: @checksum, validate_checksum: true)
|
|
76
|
+
path = export.download
|
|
77
|
+
assert(File.exist?(path))
|
|
78
|
+
assert_equal(@csv_content, File.read(path))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_download_raises_and_removes_the_file_when_checksum_does_not_match
|
|
82
|
+
export = build_export(checksum: "0" * 32, validate_checksum: true)
|
|
83
|
+
error = assert_raises(E621ExportDownloader::ResolveError) { export.download }
|
|
84
|
+
assert_match(/Checksum mismatch/, error.message)
|
|
85
|
+
assert_empty(Dir.children(E621ExportDownloader::Constants::TEMP_DIR))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_download_ignores_checksum_mismatch_when_validation_is_disabled
|
|
89
|
+
export = build_export(checksum: "0" * 32, validate_checksum: false)
|
|
90
|
+
path = export.download
|
|
91
|
+
assert(File.exist?(path))
|
|
92
|
+
end
|
|
93
|
+
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: e621_export_downloader
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.18
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Donovan_DMC
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-07-05 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: csv
|
|
@@ -223,6 +223,7 @@ files:
|
|
|
223
223
|
- sorbet/rbi/todo.rbi
|
|
224
224
|
- sorbet/tapioca/config.yml
|
|
225
225
|
- sorbet/tapioca/require.rb
|
|
226
|
+
- test/export_test.rb
|
|
226
227
|
- test/generators/install_generator_test.rb
|
|
227
228
|
homepage: https://github.com/DonovanDMC/E621ExportDownloader.rb
|
|
228
229
|
licenses:
|