e621_export_downloader 0.0.16 → 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 +85 -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 +88 -86
- data/scripts/generate_changelog.rb +50 -0
- data/test/export_test.rb +93 -0
- metadata +5 -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
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.0.18] - 2026-07-02
|
|
4
|
+
|
|
5
|
+
_0.0.18_
|
|
6
|
+
|
|
7
|
+
## [0.0.17] - 2026-06-29
|
|
8
|
+
|
|
9
|
+
_0.0.17_
|
|
10
|
+
|
|
11
|
+
## [0.0.16] - 2026-06-24
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Add new dbexport fields (from: e621ng/e621ng#2181, pending: e621ng/e621ng#2222)
|
|
15
|
+
|
|
16
|
+
## [0.0.15] - 2026-06-14
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Add method to track download progress
|
|
20
|
+
|
|
21
|
+
## [0.0.14] - 2026-06-14
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- optimize csv import
|
|
25
|
+
|
|
26
|
+
## [0.0.13] - 2026-06-13
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- Add stored row count & text indexes
|
|
30
|
+
|
|
31
|
+
## [0.0.12] - 2026-06-12
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- Fix post versions missing post_id
|
|
35
|
+
|
|
36
|
+
## [0.0.11] - 2026-06-12
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- Fix csv import
|
|
40
|
+
|
|
41
|
+
## [0.0.10] - 2026-06-12
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- Fix models
|
|
45
|
+
|
|
46
|
+
## [0.0.9] - 2026-06-12
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
- Fix parsing
|
|
50
|
+
|
|
51
|
+
## [0.0.8] - 2026-06-12
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
- Add csv import function to models
|
|
55
|
+
|
|
56
|
+
## [0.0.7] - 2026-06-12
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- Fix railtie
|
|
60
|
+
|
|
61
|
+
## [0.0.6] - 2026-06-12
|
|
62
|
+
|
|
63
|
+
_Rails Migration & models_
|
|
64
|
+
|
|
65
|
+
## [0.0.5] - 2026-06-12
|
|
66
|
+
|
|
67
|
+
_force republish_
|
|
68
|
+
|
|
69
|
+
## [0.0.4] - 2026-06-12
|
|
70
|
+
|
|
71
|
+
_db export changes_
|
|
72
|
+
|
|
73
|
+
## [0.0.3] - 2026-05-19
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
- Add activejob serializer
|
|
77
|
+
|
|
78
|
+
## [0.0.2] - 2026-05-05
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
- Fix Types serialization
|
|
82
|
+
|
|
83
|
+
## [0.0.1] - 2026-05-05
|
|
84
|
+
|
|
85
|
+
_Initial Commit_
|
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")
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
class CreateE621Tables < ActiveRecord::Migration<%= migration_version %>
|
|
4
4
|
def change
|
|
5
|
-
enable_extension("pg_trgm")
|
|
6
|
-
|
|
7
5
|
<% if use_schema? -%>
|
|
8
6
|
reversible do |r|
|
|
9
7
|
r.up { execute("CREATE SCHEMA <%= schema %>") }
|
|
@@ -13,48 +11,46 @@ class CreateE621Tables < ActiveRecord::Migration<%= migration_version %>
|
|
|
13
11
|
<% end -%>
|
|
14
12
|
create_table(<%= table_ref("artists") %>, id: :bigint, default: nil, force: :cascade) do |t|
|
|
15
13
|
t.datetime(:created_at, null: false)
|
|
16
|
-
t.bigint(:creator_id, null: false
|
|
14
|
+
t.bigint(:creator_id, null: false)
|
|
17
15
|
t.string(:group_name)
|
|
18
16
|
t.boolean(:is_active, null: false)
|
|
19
17
|
t.boolean(:is_locked, null: false)
|
|
20
|
-
t.bigint(:linked_user_id
|
|
21
|
-
t.string(:name, null: false
|
|
18
|
+
t.bigint(:linked_user_id)
|
|
19
|
+
t.string(:name, null: false)
|
|
22
20
|
t.text(:other_names, array: true, null: false, default: [])
|
|
23
21
|
t.datetime(:updated_at, null: false)
|
|
24
|
-
t.text(:
|
|
25
|
-
t.
|
|
26
|
-
t.index("name gin_trgm_ops", using: :gin)
|
|
22
|
+
t.text(:active_urls, null: false)
|
|
23
|
+
t.text(:inactive_urls, null: false)
|
|
27
24
|
end
|
|
28
25
|
|
|
29
26
|
create_table(<%= table_ref("bulk_update_requests") %>, id: :bigint, default: nil, force: :cascade) do |t|
|
|
30
|
-
t.bigint(:approver_id
|
|
27
|
+
t.bigint(:approver_id)
|
|
31
28
|
t.datetime(:created_at, null: false)
|
|
32
|
-
t.bigint(:forum_topic_id
|
|
33
|
-
t.bigint(:forum_post_id
|
|
29
|
+
t.bigint(:forum_topic_id)
|
|
30
|
+
t.bigint(:forum_post_id)
|
|
34
31
|
t.text(:script, null: false)
|
|
35
|
-
t.string(:status, null: false
|
|
32
|
+
t.string(:status, null: false)
|
|
36
33
|
t.string(:title)
|
|
37
34
|
t.datetime(:updated_at, null: false)
|
|
38
|
-
t.bigint(:user_id, null: false
|
|
39
|
-
t.
|
|
40
|
-
t.
|
|
35
|
+
t.bigint(:user_id, null: false)
|
|
36
|
+
t.integer(:down_votes, null: false)
|
|
37
|
+
t.integer(:meh_votes, null: false)
|
|
38
|
+
t.integer(:up_votes, null: false)
|
|
41
39
|
end
|
|
42
40
|
|
|
43
41
|
create_table(<%= table_ref("pools") %>, id: :bigint, default: nil, force: :cascade) do |t|
|
|
44
42
|
t.string(:category, null: false)
|
|
45
43
|
t.datetime(:created_at, null: false)
|
|
46
|
-
t.bigint(:creator_id, null: false
|
|
44
|
+
t.bigint(:creator_id, null: false)
|
|
47
45
|
t.text(:description, null: false)
|
|
48
46
|
t.boolean(:is_active, null: false)
|
|
49
|
-
t.string(:name, null: false
|
|
50
|
-
t.bigint(:post_ids, array: true, null: false
|
|
47
|
+
t.string(:name, null: false)
|
|
48
|
+
t.bigint(:post_ids, array: true, null: false)
|
|
51
49
|
t.datetime(:updated_at)
|
|
52
|
-
t.index("description gin_trgm_ops", using: :gin)
|
|
53
|
-
t.index("name gin_trgm_ops", using: :gin)
|
|
54
50
|
end
|
|
55
51
|
|
|
56
52
|
create_table(<%= table_ref("posts") %>, id: :bigint, default: nil, force: :cascade) do |t|
|
|
57
|
-
t.bigint(:approver_id
|
|
53
|
+
t.bigint(:approver_id)
|
|
58
54
|
t.bigint(:change_seq, null: false)
|
|
59
55
|
t.integer(:comment_count, null: false)
|
|
60
56
|
t.datetime(:created_at, null: false)
|
|
@@ -66,112 +62,118 @@ class CreateE621Tables < ActiveRecord::Migration<%= migration_version %>
|
|
|
66
62
|
t.bigint(:file_size, null: false)
|
|
67
63
|
t.integer(:image_height, null: false)
|
|
68
64
|
t.integer(:image_width, null: false)
|
|
69
|
-
t.boolean(:is_deleted, null: false
|
|
70
|
-
t.boolean(:is_flagged, null: false
|
|
71
|
-
t.boolean(:is_note_locked, null: false
|
|
72
|
-
t.boolean(:is_pending, null: false
|
|
73
|
-
t.boolean(:is_rating_locked, null: false
|
|
74
|
-
t.boolean(:is_status_locked, null: false
|
|
75
|
-
t.text(:locked_tags
|
|
76
|
-
t.string(:md5,
|
|
77
|
-
t.bigint(:parent_id
|
|
78
|
-
t.string(:rating, null: false
|
|
65
|
+
t.boolean(:is_deleted, null: false)
|
|
66
|
+
t.boolean(:is_flagged, null: false)
|
|
67
|
+
t.boolean(:is_note_locked, null: false)
|
|
68
|
+
t.boolean(:is_pending, null: false)
|
|
69
|
+
t.boolean(:is_rating_locked, null: false)
|
|
70
|
+
t.boolean(:is_status_locked, null: false)
|
|
71
|
+
t.text(:locked_tags)
|
|
72
|
+
t.string(:md5, null: false)
|
|
73
|
+
t.bigint(:parent_id)
|
|
74
|
+
t.string(:rating, null: false)
|
|
79
75
|
t.integer(:score, null: false)
|
|
80
76
|
t.text(:source, null: false)
|
|
81
77
|
t.text(:tag_string, null: false)
|
|
82
78
|
t.integer(:up_score, null: false)
|
|
83
|
-
t.datetime(:updated_at
|
|
84
|
-
t.bigint(:uploader_id
|
|
85
|
-
t.
|
|
86
|
-
t.
|
|
87
|
-
t.
|
|
79
|
+
t.datetime(:updated_at)
|
|
80
|
+
t.bigint(:uploader_id)
|
|
81
|
+
t.string(:bg_color)
|
|
82
|
+
t.datetime(:last_noted_at)
|
|
83
|
+
t.datetime(:last_commented_at)
|
|
88
84
|
end
|
|
89
85
|
|
|
90
86
|
create_table(<%= table_ref("post_replacements") %>, id: :bigint, default: nil, force: :cascade) do |t|
|
|
91
|
-
t.bigint(:approver_id
|
|
87
|
+
t.bigint(:approver_id)
|
|
92
88
|
t.datetime(:created_at, null: false)
|
|
93
|
-
t.bigint(:creator_id, null: false
|
|
94
|
-
t.string(:file_ext, null: false
|
|
95
|
-
t.string(:file_name, null: false
|
|
96
|
-
t.bigint(:file_size, null: false
|
|
97
|
-
t.integer(:image_height, null: false
|
|
98
|
-
t.integer(:image_width, null: false
|
|
99
|
-
t.string(:md5, null: false
|
|
100
|
-
t.bigint(:post_id, null: false
|
|
89
|
+
t.bigint(:creator_id, null: false)
|
|
90
|
+
t.string(:file_ext, null: false)
|
|
91
|
+
t.string(:file_name, null: false)
|
|
92
|
+
t.bigint(:file_size, null: false)
|
|
93
|
+
t.integer(:image_height, null: false)
|
|
94
|
+
t.integer(:image_width, null: false)
|
|
95
|
+
t.string(:md5, null: false)
|
|
96
|
+
t.bigint(:post_id, null: false)
|
|
101
97
|
t.text(:reason, null: false)
|
|
102
98
|
t.text(:source)
|
|
103
|
-
t.string(:status, null: false
|
|
99
|
+
t.string(:status, null: false)
|
|
104
100
|
t.datetime(:updated_at, null: false)
|
|
105
|
-
t.index("reason gin_trgm_ops", using: :gin)
|
|
106
|
-
t.index("source gin_trgm_ops", using: :gin)
|
|
107
101
|
end
|
|
108
102
|
|
|
109
103
|
create_table(<%= table_ref("post_versions") %>, id: :bigint, default: nil, force: :cascade) do |t|
|
|
110
104
|
t.text(:added_locked_tags, array: true, null: false, default: [])
|
|
111
105
|
t.text(:added_tags, array: true, null: false, default: [])
|
|
112
106
|
t.text(:description)
|
|
113
|
-
t.boolean(:description_changed, null: false
|
|
107
|
+
t.boolean(:description_changed, null: false)
|
|
114
108
|
t.text(:locked_tags)
|
|
115
|
-
t.boolean(:parent_changed, null: false
|
|
109
|
+
t.boolean(:parent_changed, null: false)
|
|
116
110
|
t.bigint(:parent_id)
|
|
117
|
-
t.bigint(:post_id, null: false
|
|
111
|
+
t.bigint(:post_id, null: false)
|
|
118
112
|
t.string(:rating)
|
|
119
|
-
t.boolean(:rating_changed, null: false
|
|
113
|
+
t.boolean(:rating_changed, null: false)
|
|
120
114
|
t.text(:reason)
|
|
121
115
|
t.text(:removed_locked_tags, array: true, null: false, default: [])
|
|
122
116
|
t.text(:removed_tags, array: true, null: false, default: [])
|
|
123
117
|
t.text(:source)
|
|
124
|
-
t.boolean(:source_changed, null: false
|
|
118
|
+
t.boolean(:source_changed, null: false)
|
|
125
119
|
t.text(:tags)
|
|
126
|
-
t.datetime(:updated_at, null: false
|
|
127
|
-
t.bigint(:updater_id
|
|
128
|
-
t.integer(:version, null: false
|
|
129
|
-
t.index("description gin_trgm_ops", using: :gin)
|
|
130
|
-
t.index("locked_tags gin_trgm_ops", using: :gin)
|
|
131
|
-
t.index("reason gin_trgm_ops", using: :gin)
|
|
132
|
-
t.index("source gin_trgm_ops", using: :gin)
|
|
133
|
-
t.index("tags gin_trgm_ops", using: :gin)
|
|
120
|
+
t.datetime(:updated_at, null: false)
|
|
121
|
+
t.bigint(:updater_id)
|
|
122
|
+
t.integer(:version, null: false)
|
|
134
123
|
end
|
|
135
124
|
|
|
136
125
|
create_table(<%= table_ref("tag_aliases") %>, id: :bigint, default: nil, force: :cascade) do |t|
|
|
137
|
-
t.string(:antecedent_name, null: false
|
|
138
|
-
t.string(:consequent_name, null: false
|
|
126
|
+
t.string(:antecedent_name, null: false)
|
|
127
|
+
t.string(:consequent_name, null: false)
|
|
139
128
|
t.datetime(:created_at)
|
|
140
|
-
t.string(:status, null: false
|
|
141
|
-
t.
|
|
142
|
-
t.
|
|
143
|
-
t.
|
|
129
|
+
t.string(:status, null: false)
|
|
130
|
+
t.integer(:forum_post_id, null: false)
|
|
131
|
+
t.integer(:forum_topic_id, null: false)
|
|
132
|
+
t.string(:reason, null: false)
|
|
133
|
+
t.datetime(:updated_at)
|
|
134
|
+
t.integer(:approver_id)
|
|
135
|
+
t.integer(:post_count, null: false)
|
|
136
|
+
t.integer(:down_votes, null: false)
|
|
137
|
+
t.integer(:meh_votes, null: false)
|
|
138
|
+
t.integer(:up_votes, null: false)
|
|
139
|
+
t.integer(:creator_id, null: false)
|
|
144
140
|
end
|
|
145
141
|
|
|
146
142
|
create_table(<%= table_ref("tag_implications") %>, id: :bigint, default: nil, force: :cascade) do |t|
|
|
147
|
-
t.string(:antecedent_name, null: false
|
|
148
|
-
t.string(:consequent_name, null: false
|
|
143
|
+
t.string(:antecedent_name, null: false)
|
|
144
|
+
t.string(:consequent_name, null: false)
|
|
149
145
|
t.datetime(:created_at)
|
|
150
|
-
t.string(:status, null: false
|
|
151
|
-
t.
|
|
152
|
-
t.
|
|
153
|
-
t.
|
|
154
|
-
t.
|
|
146
|
+
t.string(:status, null: false)
|
|
147
|
+
t.integer(:forum_post_id, null: false)
|
|
148
|
+
t.integer(:forum_topic_id, null: false)
|
|
149
|
+
t.string(:reason, null: false)
|
|
150
|
+
t.datetime(:updated_at)
|
|
151
|
+
t.integer(:approver_id)
|
|
152
|
+
t.string(:descendant_names, array: true)
|
|
153
|
+
t.integer(:down_votes, null: false)
|
|
154
|
+
t.integer(:meh_votes, null: false)
|
|
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|
|
|
158
|
-
t.string(:category, null: false
|
|
159
|
-
t.string(:name, null: false
|
|
160
|
-
t.integer(:post_count, null: false
|
|
161
|
-
t.
|
|
162
|
-
t.
|
|
160
|
+
t.string(:category, null: false)
|
|
161
|
+
t.string(:name, null: false)
|
|
162
|
+
t.integer(:post_count, null: false)
|
|
163
|
+
t.datetime(:created_at, null: false)
|
|
164
|
+
t.datetime(:updated_at, null: false)
|
|
165
|
+
t.boolean(:is_locked, null: false)
|
|
163
166
|
end
|
|
164
167
|
|
|
165
168
|
create_table(<%= table_ref("wiki_pages") %>, id: :bigint, default: nil, force: :cascade) do |t|
|
|
166
|
-
t.text(:body, null: false
|
|
169
|
+
t.text(:body, null: false)
|
|
167
170
|
t.datetime(:created_at, null: false)
|
|
168
|
-
t.bigint(:creator_id, null: false
|
|
169
|
-
t.boolean(:is_locked, null: false
|
|
170
|
-
t.string(:title, null: false
|
|
171
|
+
t.bigint(:creator_id, null: false)
|
|
172
|
+
t.boolean(:is_locked, null: false)
|
|
173
|
+
t.string(:title, null: false)
|
|
171
174
|
t.datetime(:updated_at, null: false)
|
|
172
|
-
t.bigint(:updater_id
|
|
173
|
-
t.
|
|
174
|
-
t.index("title gin_trgm_ops", using: :gin)
|
|
175
|
+
t.bigint(:updater_id)
|
|
176
|
+
t.string(:parent)
|
|
175
177
|
end
|
|
176
178
|
|
|
177
179
|
create_table(<%= table_ref("row_counts") %>, primary_key: :table_name, id: :string, force: :cascade) do |t|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require("date")
|
|
5
|
+
|
|
6
|
+
def categorize(subject)
|
|
7
|
+
case subject
|
|
8
|
+
when /\Aadd /i then "Added"
|
|
9
|
+
when /\Afix /i then "Fixed"
|
|
10
|
+
when /\Aoptimize /i, /\Arefactor /i, /\Aupdate /i then "Changed"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
shas = `git log --follow --format="%H" -- lib/e621_export_downloader/version.rb`.strip.split("\n")
|
|
15
|
+
|
|
16
|
+
entries = shas.map do |sha|
|
|
17
|
+
subject = `git log -1 --format="%s" #{sha}`.strip
|
|
18
|
+
date_str = `git log -1 --format="%ai" #{sha}`.strip
|
|
19
|
+
body = `git log -1 --format="%b" #{sha}`.strip
|
|
20
|
+
version_file = `git show #{sha}:lib/e621_export_downloader/version.rb`
|
|
21
|
+
version = version_file.match(/VERSION = "([^"]+)"/)[1]
|
|
22
|
+
date = Date.parse(date_str).strftime("%Y-%m-%d")
|
|
23
|
+
section = categorize(subject)
|
|
24
|
+
|
|
25
|
+
{ version: version, date: date, section: section, subject: subject, body: body }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
changelog = +"# Changelog\n"
|
|
29
|
+
|
|
30
|
+
entries.each do |e|
|
|
31
|
+
changelog << "\n## [#{e[:version]}] - #{e[:date]}\n"
|
|
32
|
+
|
|
33
|
+
section = e[:section]
|
|
34
|
+
if section
|
|
35
|
+
changelog << "\n### #{section}\n"
|
|
36
|
+
changelog << "- #{e[:subject]}"
|
|
37
|
+
|
|
38
|
+
unless e[:body].empty?
|
|
39
|
+
refs = e[:body].scan(/(?:from|pending):\s*\S+/).join(", ")
|
|
40
|
+
changelog << " (#{refs})" unless refs.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
changelog << "\n"
|
|
44
|
+
else
|
|
45
|
+
changelog << "\n_#{e[:subject]}_\n"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
File.write("CHANGELOG.md", changelog)
|
|
50
|
+
puts("Generated CHANGELOG.md")
|
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
|
|
@@ -75,6 +75,7 @@ extra_rdoc_files: []
|
|
|
75
75
|
files:
|
|
76
76
|
- ".irbrc"
|
|
77
77
|
- ".ruby-version"
|
|
78
|
+
- CHANGELOG.md
|
|
78
79
|
- LICENSE
|
|
79
80
|
- README.md
|
|
80
81
|
- Rakefile
|
|
@@ -118,6 +119,7 @@ files:
|
|
|
118
119
|
- lib/generators/e621_export_downloader/install_generator.rb
|
|
119
120
|
- lib/generators/e621_export_downloader/templates/initializers/e621_models.rb.erb
|
|
120
121
|
- lib/generators/e621_export_downloader/templates/migrations/create_e621_tables.rb.erb
|
|
122
|
+
- scripts/generate_changelog.rb
|
|
121
123
|
- sorbet/config
|
|
122
124
|
- sorbet/rbi/annotations/.gitattributes
|
|
123
125
|
- sorbet/rbi/annotations/faraday.rbi
|
|
@@ -221,6 +223,7 @@ files:
|
|
|
221
223
|
- sorbet/rbi/todo.rbi
|
|
222
224
|
- sorbet/tapioca/config.yml
|
|
223
225
|
- sorbet/tapioca/require.rb
|
|
226
|
+
- test/export_test.rb
|
|
224
227
|
- test/generators/install_generator_test.rb
|
|
225
228
|
homepage: https://github.com/DonovanDMC/E621ExportDownloader.rb
|
|
226
229
|
licenses:
|