factorix 0.6.0 → 0.7.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/CHANGELOG.md +26 -0
- data/exe/factorix +17 -0
- data/lib/factorix/api/mod_download_api.rb +10 -5
- data/lib/factorix/api/mod_portal_api.rb +6 -49
- data/lib/factorix/cache/base.rb +116 -0
- data/lib/factorix/cache/entry.rb +25 -0
- data/lib/factorix/cache/file_system.rb +137 -57
- data/lib/factorix/cache/redis.rb +287 -0
- data/lib/factorix/cache/s3.rb +388 -0
- data/lib/factorix/cli/commands/cache/evict.rb +17 -22
- data/lib/factorix/cli/commands/cache/stat.rb +57 -58
- data/lib/factorix/cli/commands/download_support.rb +1 -6
- data/lib/factorix/cli/commands/mod/download.rb +2 -3
- data/lib/factorix/cli/commands/mod/edit.rb +1 -4
- data/lib/factorix/cli/commands/mod/image/add.rb +1 -4
- data/lib/factorix/cli/commands/mod/image/edit.rb +1 -4
- data/lib/factorix/cli/commands/mod/image/list.rb +1 -4
- data/lib/factorix/cli/commands/mod/install.rb +2 -3
- data/lib/factorix/cli/commands/mod/search.rb +2 -3
- data/lib/factorix/cli/commands/mod/show.rb +2 -3
- data/lib/factorix/cli/commands/mod/sync.rb +2 -3
- data/lib/factorix/cli/commands/mod/update.rb +6 -39
- data/lib/factorix/cli/commands/mod/upload.rb +1 -4
- data/lib/factorix/cli/commands/portal_support.rb +27 -0
- data/lib/factorix/container.rb +27 -13
- data/lib/factorix/errors.rb +3 -0
- data/lib/factorix/http/cache_decorator.rb +5 -5
- data/lib/factorix/info_json.rb +5 -5
- data/lib/factorix/portal.rb +3 -2
- data/lib/factorix/transfer/downloader.rb +19 -11
- data/lib/factorix/version.rb +1 -1
- data/lib/factorix.rb +45 -53
- data/sig/factorix/api/mod_download_api.rbs +1 -2
- data/sig/factorix/cache/base.rbs +28 -0
- data/sig/factorix/cache/entry.rbs +14 -0
- data/sig/factorix/cache/file_system.rbs +7 -6
- data/sig/factorix/cache/redis.rbs +36 -0
- data/sig/factorix/cache/s3.rbs +38 -0
- data/sig/factorix/errors.rbs +3 -0
- data/sig/factorix/portal.rbs +1 -1
- metadata +25 -2
|
@@ -48,7 +48,6 @@ module Factorix
|
|
|
48
48
|
def call(caches: nil, all: false, expired: false, older_than: nil, **)
|
|
49
49
|
validate_options!(all, expired, older_than)
|
|
50
50
|
|
|
51
|
-
@now = Time.now
|
|
52
51
|
@older_than_seconds = parse_age(older_than) if older_than
|
|
53
52
|
|
|
54
53
|
cache_names = resolve_cache_names(caches)
|
|
@@ -114,25 +113,26 @@ module Factorix
|
|
|
114
113
|
# @param expired [Boolean] remove expired entries only
|
|
115
114
|
# @return [Hash] eviction result with :count and :size
|
|
116
115
|
private def evict_cache(name, all:, expired:)
|
|
117
|
-
|
|
118
|
-
cache_dir = config.dir
|
|
119
|
-
ttl = config.ttl
|
|
120
|
-
|
|
121
|
-
return {count: 0, size: 0} unless cache_dir.exist?
|
|
116
|
+
cache = Container.resolve(:"#{name}_cache")
|
|
122
117
|
|
|
123
118
|
count = 0
|
|
124
119
|
size = 0
|
|
125
120
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
# Collect keys to evict (we can't modify during iteration)
|
|
122
|
+
to_evict = []
|
|
123
|
+
cache.each do |key, entry|
|
|
124
|
+
next unless should_evict?(entry, all:, expired:)
|
|
125
|
+
|
|
126
|
+
to_evict << [key, entry.size]
|
|
127
|
+
end
|
|
129
128
|
|
|
130
|
-
|
|
129
|
+
# Perform eviction
|
|
130
|
+
to_evict.each do |key, entry_size|
|
|
131
|
+
next unless cache.delete(key)
|
|
131
132
|
|
|
132
|
-
size += path.size
|
|
133
|
-
path.delete
|
|
134
133
|
count += 1
|
|
135
|
-
|
|
134
|
+
size += entry_size
|
|
135
|
+
logger.debug("Evicted cache entry", key:)
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
logger.info("Evicted cache entries", cache: name, count:, size:)
|
|
@@ -141,23 +141,18 @@ module Factorix
|
|
|
141
141
|
|
|
142
142
|
# Determine if a cache entry should be evicted
|
|
143
143
|
#
|
|
144
|
-
# @param
|
|
145
|
-
# @param ttl [Integer, nil] cache TTL
|
|
144
|
+
# @param entry [Cache::Entry] cache entry
|
|
146
145
|
# @param all [Boolean] remove all entries
|
|
147
146
|
# @param expired [Boolean] remove expired entries only
|
|
148
147
|
# @return [Boolean] true if entry should be evicted
|
|
149
|
-
private def should_evict?(
|
|
148
|
+
private def should_evict?(entry, all:, expired:)
|
|
150
149
|
return true if all
|
|
151
150
|
|
|
152
|
-
age_seconds = @now - path.mtime
|
|
153
|
-
|
|
154
151
|
if expired
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
age_seconds > ttl
|
|
152
|
+
entry.expired?
|
|
158
153
|
else
|
|
159
154
|
# --older-than
|
|
160
|
-
|
|
155
|
+
entry.age > @older_than_seconds
|
|
161
156
|
end
|
|
162
157
|
end
|
|
163
158
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "dry/inflector"
|
|
3
4
|
require "json"
|
|
4
5
|
|
|
5
6
|
module Factorix
|
|
@@ -8,13 +9,12 @@ module Factorix
|
|
|
8
9
|
module Cache
|
|
9
10
|
# Display cache statistics
|
|
10
11
|
#
|
|
11
|
-
# This command
|
|
12
|
+
# This command outputs statistics for all cache stores
|
|
12
13
|
# in a human-readable or JSON format.
|
|
13
14
|
#
|
|
14
15
|
# @example
|
|
15
16
|
# $ factorix cache stat
|
|
16
17
|
# download:
|
|
17
|
-
# Directory: ~/.cache/factorix/download
|
|
18
18
|
# TTL: unlimited
|
|
19
19
|
# Entries: 42 / 42 (100.0% valid)
|
|
20
20
|
# ...
|
|
@@ -41,7 +41,6 @@ module Factorix
|
|
|
41
41
|
def call(json:, **)
|
|
42
42
|
logger.debug("Collecting cache statistics")
|
|
43
43
|
|
|
44
|
-
@now = Time.now
|
|
45
44
|
cache_names = Factorix.config.cache.values.keys
|
|
46
45
|
stats = cache_names.to_h {|name| [name, collect_stats(name)] }
|
|
47
46
|
|
|
@@ -52,52 +51,42 @@ module Factorix
|
|
|
52
51
|
end
|
|
53
52
|
end
|
|
54
53
|
|
|
54
|
+
# Collect statistics for a cache
|
|
55
|
+
#
|
|
56
|
+
# @param name [Symbol] cache name
|
|
57
|
+
# @return [Hash] cache statistics
|
|
55
58
|
private def collect_stats(name)
|
|
59
|
+
cache = Container.resolve(:"#{name}_cache")
|
|
56
60
|
config = Factorix.config.cache.public_send(name)
|
|
57
|
-
cache_dir = config.dir
|
|
58
61
|
|
|
59
|
-
entries = scan_entries(
|
|
62
|
+
entries = scan_entries(cache)
|
|
60
63
|
|
|
61
64
|
{
|
|
62
|
-
directory: cache_dir.to_s,
|
|
63
65
|
ttl: config.ttl,
|
|
64
|
-
max_file_size: config.max_file_size,
|
|
65
|
-
compression_threshold: config.compression_threshold,
|
|
66
66
|
entries: build_entry_stats(entries),
|
|
67
67
|
size: build_size_stats(entries),
|
|
68
68
|
age: build_age_stats(entries),
|
|
69
|
-
|
|
69
|
+
backend_info: cache.backend_info
|
|
70
70
|
}
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
#
|
|
73
|
+
# Collect cache entries using the cache interface
|
|
74
74
|
#
|
|
75
|
-
# @param
|
|
76
|
-
# @
|
|
77
|
-
|
|
78
|
-
private def scan_entries(cache_dir, ttl)
|
|
79
|
-
return [] unless cache_dir.exist?
|
|
80
|
-
|
|
75
|
+
# @param cache [Cache::Base] cache instance
|
|
76
|
+
# @return [Array<Cache::Entry>] array of cache entries
|
|
77
|
+
private def scan_entries(cache)
|
|
81
78
|
entries = []
|
|
82
|
-
|
|
83
|
-
next unless path.file?
|
|
84
|
-
next if path.extname == ".lock"
|
|
85
|
-
|
|
86
|
-
age_seconds = @now - path.mtime
|
|
87
|
-
expired = ttl ? age_seconds > ttl : false
|
|
88
|
-
|
|
89
|
-
entries << {size: path.size, age: age_seconds, expired:}
|
|
90
|
-
end
|
|
79
|
+
cache.each {|_key, entry| entries << entry }
|
|
91
80
|
entries
|
|
92
81
|
end
|
|
93
82
|
|
|
94
83
|
# Build entry count statistics
|
|
95
84
|
#
|
|
96
|
-
# @param entries [Array<
|
|
85
|
+
# @param entries [Array<Cache::Entry>] entry array
|
|
97
86
|
# @return [Hash] entry statistics
|
|
98
87
|
private def build_entry_stats(entries)
|
|
99
88
|
total = entries.size
|
|
100
|
-
valid = entries.count {|e| !e
|
|
89
|
+
valid = entries.count {|e| !e.expired? }
|
|
101
90
|
expired = total - valid
|
|
102
91
|
|
|
103
92
|
{total:, valid:, expired:}
|
|
@@ -105,37 +94,26 @@ module Factorix
|
|
|
105
94
|
|
|
106
95
|
# Build size statistics
|
|
107
96
|
#
|
|
108
|
-
# @param entries [Array<
|
|
97
|
+
# @param entries [Array<Cache::Entry>] entry array
|
|
109
98
|
# @return [Hash] size statistics
|
|
110
99
|
private def build_size_stats(entries)
|
|
111
100
|
return {total: 0, avg: 0, min: 0, max: 0} if entries.empty?
|
|
112
101
|
|
|
113
|
-
sizes = entries.map
|
|
102
|
+
sizes = entries.map(&:size)
|
|
114
103
|
{total: sizes.sum, avg: sizes.sum / sizes.size, min: sizes.min, max: sizes.max}
|
|
115
104
|
end
|
|
116
105
|
|
|
117
106
|
# Build age statistics
|
|
118
107
|
#
|
|
119
|
-
# @param entries [Array<
|
|
108
|
+
# @param entries [Array<Cache::Entry>] entry array
|
|
120
109
|
# @return [Hash] age statistics
|
|
121
110
|
private def build_age_stats(entries)
|
|
122
111
|
return {oldest: nil, newest: nil, avg: nil} if entries.empty?
|
|
123
112
|
|
|
124
|
-
ages = entries.map
|
|
113
|
+
ages = entries.map(&:age)
|
|
125
114
|
{oldest: ages.max, newest: ages.min, avg: ages.sum / ages.size}
|
|
126
115
|
end
|
|
127
116
|
|
|
128
|
-
# Count stale lock files
|
|
129
|
-
#
|
|
130
|
-
# @param cache_dir [Pathname] cache directory path
|
|
131
|
-
# @return [Integer] number of stale lock files
|
|
132
|
-
private def count_stale_locks(cache_dir)
|
|
133
|
-
return 0 unless cache_dir.exist?
|
|
134
|
-
|
|
135
|
-
lock_lifetime = Factorix::Cache::FileSystem::LOCK_FILE_LIFETIME
|
|
136
|
-
cache_dir.glob("**/*.lock").count {|path| @now - path.mtime > lock_lifetime }
|
|
137
|
-
end
|
|
138
|
-
|
|
139
117
|
# Output statistics in text format (ccache-style)
|
|
140
118
|
#
|
|
141
119
|
# @param stats [Hash] statistics for all caches
|
|
@@ -153,10 +131,7 @@ module Factorix
|
|
|
153
131
|
# @param data [Hash] cache statistics
|
|
154
132
|
# @return [void]
|
|
155
133
|
private def output_cache_stats(data)
|
|
156
|
-
out.puts " Directory: #{data[:directory]}"
|
|
157
134
|
out.puts " TTL: #{format_ttl(data[:ttl])}"
|
|
158
|
-
out.puts " Max file size: #{format_size(data[:max_file_size])}"
|
|
159
|
-
out.puts " Compression: #{format_compression(data[:compression_threshold])}"
|
|
160
135
|
|
|
161
136
|
entries = data[:entries]
|
|
162
137
|
valid_pct = entries[:total] > 0 ? (Float(entries[:valid]) / entries[:total] * 100) : 0.0
|
|
@@ -172,7 +147,43 @@ module Factorix
|
|
|
172
147
|
out.puts " Age: -"
|
|
173
148
|
end
|
|
174
149
|
|
|
175
|
-
|
|
150
|
+
output_backend_info(data[:backend_info])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
INFLECTOR = Dry::Inflector.new do |inflections|
|
|
154
|
+
inflections.acronym("URL")
|
|
155
|
+
end
|
|
156
|
+
private_constant :INFLECTOR
|
|
157
|
+
|
|
158
|
+
# Output backend-specific information
|
|
159
|
+
#
|
|
160
|
+
# @param info [Hash] backend-specific information
|
|
161
|
+
# @return [void]
|
|
162
|
+
private def output_backend_info(info)
|
|
163
|
+
return if info.empty?
|
|
164
|
+
|
|
165
|
+
out.puts " Backend:"
|
|
166
|
+
info.each do |key, value|
|
|
167
|
+
label = INFLECTOR.humanize(key)
|
|
168
|
+
formatted_value = format_backend_value(key, value)
|
|
169
|
+
out.puts " %-20s %s" % [label + ":", formatted_value]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Format a backend info value for display
|
|
174
|
+
#
|
|
175
|
+
# @param key [Symbol] the key name
|
|
176
|
+
# @param value [Object] the value to format
|
|
177
|
+
# @return [String] formatted value
|
|
178
|
+
private def format_backend_value(key, value)
|
|
179
|
+
case key
|
|
180
|
+
when :max_file_size, :compression_threshold
|
|
181
|
+
value.nil? ? "unlimited" : format_size(value)
|
|
182
|
+
when :lock_timeout
|
|
183
|
+
format_duration(value)
|
|
184
|
+
else
|
|
185
|
+
value.to_s
|
|
186
|
+
end
|
|
176
187
|
end
|
|
177
188
|
|
|
178
189
|
# Format TTL value for display
|
|
@@ -182,18 +193,6 @@ module Factorix
|
|
|
182
193
|
private def format_ttl(ttl)
|
|
183
194
|
ttl.nil? ? "unlimited" : format_duration(ttl)
|
|
184
195
|
end
|
|
185
|
-
|
|
186
|
-
# Format compression threshold for display
|
|
187
|
-
#
|
|
188
|
-
# @param threshold [Integer, nil] compression threshold in bytes
|
|
189
|
-
# @return [String] formatted compression setting
|
|
190
|
-
private def format_compression(threshold)
|
|
191
|
-
case threshold
|
|
192
|
-
when nil then "disabled"
|
|
193
|
-
when 0 then "enabled (always)"
|
|
194
|
-
else "enabled (>= #{format_size(threshold)})"
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
196
|
end
|
|
198
197
|
end
|
|
199
198
|
end
|
|
@@ -97,18 +97,13 @@ module Factorix
|
|
|
97
97
|
|
|
98
98
|
futures = targets.map {|target|
|
|
99
99
|
Concurrent::Future.execute(executor: pool) do
|
|
100
|
-
thread_portal = Container[:portal]
|
|
101
|
-
thread_downloader = thread_portal.mod_download_api.downloader
|
|
102
|
-
|
|
103
100
|
presenter = multi_presenter.register(
|
|
104
101
|
target[:mod].name,
|
|
105
102
|
title: target[:release].file_name
|
|
106
103
|
)
|
|
107
104
|
handler = Progress::DownloadHandler.new(presenter)
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
thread_portal.download_mod(target[:release], target[:output_path])
|
|
111
|
-
thread_downloader.unsubscribe(handler)
|
|
106
|
+
portal.download_mod(target[:release], target[:output_path], handler:)
|
|
112
107
|
end
|
|
113
108
|
}
|
|
114
109
|
|
|
@@ -10,14 +10,13 @@ module Factorix
|
|
|
10
10
|
# Download MOD files from Factorio MOD Portal
|
|
11
11
|
class Download < Base
|
|
12
12
|
include DownloadSupport
|
|
13
|
+
include PortalSupport
|
|
13
14
|
# @!parse
|
|
14
|
-
# # @return [Portal]
|
|
15
|
-
# attr_reader :portal
|
|
16
15
|
# # @return [Dry::Logger::Dispatcher]
|
|
17
16
|
# attr_reader :logger
|
|
18
17
|
# # @return [Runtime]
|
|
19
18
|
# attr_reader :runtime
|
|
20
|
-
include Import[:
|
|
19
|
+
include Import[:logger, :runtime]
|
|
21
20
|
|
|
22
21
|
desc "Download MOD files from Factorio MOD Portal"
|
|
23
22
|
|
|
@@ -6,10 +6,7 @@ module Factorix
|
|
|
6
6
|
module MOD
|
|
7
7
|
# Edit MOD metadata on Factorio MOD Portal
|
|
8
8
|
class Edit < Base
|
|
9
|
-
|
|
10
|
-
# # @return [Portal]
|
|
11
|
-
# attr_reader :portal
|
|
12
|
-
include Import[:portal]
|
|
9
|
+
include PortalSupport
|
|
13
10
|
|
|
14
11
|
desc "Edit MOD metadata on Factorio MOD Portal"
|
|
15
12
|
|
|
@@ -7,10 +7,7 @@ module Factorix
|
|
|
7
7
|
module Image
|
|
8
8
|
# Edit MOD's image list on Factorio MOD Portal
|
|
9
9
|
class Edit < Base
|
|
10
|
-
|
|
11
|
-
# # @return [Portal]
|
|
12
|
-
# attr_reader :portal
|
|
13
|
-
include Import[:portal]
|
|
10
|
+
include PortalSupport
|
|
14
11
|
|
|
15
12
|
desc "Edit MOD's image list (reorder/remove images)"
|
|
16
13
|
|
|
@@ -14,15 +14,14 @@ module Factorix
|
|
|
14
14
|
backup_support!
|
|
15
15
|
|
|
16
16
|
include DownloadSupport
|
|
17
|
+
include PortalSupport
|
|
17
18
|
|
|
18
19
|
# @!parse
|
|
19
|
-
# # @return [Portal]
|
|
20
|
-
# attr_reader :portal
|
|
21
20
|
# # @return [Dry::Logger::Dispatcher]
|
|
22
21
|
# attr_reader :logger
|
|
23
22
|
# # @return [Factorix::Runtime]
|
|
24
23
|
# attr_reader :runtime
|
|
25
|
-
include Import[:
|
|
24
|
+
include Import[:logger, :runtime]
|
|
26
25
|
|
|
27
26
|
desc "Install MOD(s) from Factorio MOD Portal (downloads to MOD directory and enables)"
|
|
28
27
|
|
|
@@ -8,12 +8,11 @@ module Factorix
|
|
|
8
8
|
module MOD
|
|
9
9
|
# Search MODs on Factorio MOD Portal
|
|
10
10
|
class Search < Base
|
|
11
|
+
include PortalSupport
|
|
11
12
|
# @!parse
|
|
12
|
-
# # @return [Portal]
|
|
13
|
-
# attr_reader :portal
|
|
14
13
|
# # @return [Runtime]
|
|
15
14
|
# attr_reader :runtime
|
|
16
|
-
include Import[:
|
|
15
|
+
include Import[:runtime]
|
|
17
16
|
|
|
18
17
|
desc "Search MOD(s) on Factorio MOD Portal"
|
|
19
18
|
|
|
@@ -20,11 +20,10 @@ module Factorix
|
|
|
20
20
|
INCOMPATIBLE_MOD_STYLE = TIntMe[:red]
|
|
21
21
|
private_constant :INCOMPATIBLE_MOD_STYLE
|
|
22
22
|
# @!parse
|
|
23
|
-
# # @return [Portal]
|
|
24
|
-
# attr_reader :portal
|
|
25
23
|
# # @return [Runtime]
|
|
26
24
|
# attr_reader :runtime
|
|
27
|
-
include Import[:
|
|
25
|
+
include Import[:runtime]
|
|
26
|
+
include PortalSupport
|
|
28
27
|
|
|
29
28
|
desc "Show MOD details from Factorio MOD Portal"
|
|
30
29
|
|
|
@@ -14,15 +14,14 @@ module Factorix
|
|
|
14
14
|
backup_support!
|
|
15
15
|
|
|
16
16
|
include DownloadSupport
|
|
17
|
+
include PortalSupport
|
|
17
18
|
|
|
18
19
|
# @!parse
|
|
19
|
-
# # @return [Portal]
|
|
20
|
-
# attr_reader :portal
|
|
21
20
|
# # @return [Dry::Logger::Dispatcher]
|
|
22
21
|
# attr_reader :logger
|
|
23
22
|
# # @return [Factorix::Runtime]
|
|
24
23
|
# attr_reader :runtime
|
|
25
|
-
include Import[:
|
|
24
|
+
include Import[:logger, :runtime]
|
|
26
25
|
|
|
27
26
|
desc "Sync MOD states and startup settings from a save file"
|
|
28
27
|
|
|
@@ -13,14 +13,14 @@ module Factorix
|
|
|
13
13
|
require_game_stopped!
|
|
14
14
|
backup_support!
|
|
15
15
|
|
|
16
|
+
include PortalSupport
|
|
17
|
+
include DownloadSupport
|
|
16
18
|
# @!parse
|
|
17
|
-
# # @return [Portal]
|
|
18
|
-
# attr_reader :portal
|
|
19
19
|
# # @return [Dry::Logger::Dispatcher]
|
|
20
20
|
# attr_reader :logger
|
|
21
21
|
# # @return [Factorix::Runtime]
|
|
22
22
|
# attr_reader :runtime
|
|
23
|
-
include Import[:
|
|
23
|
+
include Import[:logger, :runtime]
|
|
24
24
|
|
|
25
25
|
desc "Update MOD(s) to their latest versions"
|
|
26
26
|
|
|
@@ -141,7 +141,7 @@ module Factorix
|
|
|
141
141
|
mod:,
|
|
142
142
|
mod_info:,
|
|
143
143
|
current_version:,
|
|
144
|
-
latest_release
|
|
144
|
+
release: latest_release,
|
|
145
145
|
output_path: runtime.mod_dir / latest_release.file_name
|
|
146
146
|
}
|
|
147
147
|
rescue MODNotOnPortalError
|
|
@@ -156,7 +156,7 @@ module Factorix
|
|
|
156
156
|
private def show_plan(targets)
|
|
157
157
|
say "Planning to update #{targets.size} MOD(s):", prefix: :info
|
|
158
158
|
targets.each do |target|
|
|
159
|
-
say " - #{target[:mod]}: #{target[:current_version]} -> #{target[:
|
|
159
|
+
say " - #{target[:mod]}: #{target[:current_version]} -> #{target[:release].version}"
|
|
160
160
|
end
|
|
161
161
|
end
|
|
162
162
|
|
|
@@ -176,46 +176,13 @@ module Factorix
|
|
|
176
176
|
current_enabled = mod_list.enabled?(mod)
|
|
177
177
|
mod_list.remove(mod)
|
|
178
178
|
mod_list.add(mod, enabled: current_enabled)
|
|
179
|
-
say "Updated #{mod} to #{target[:
|
|
179
|
+
say "Updated #{mod} to #{target[:release].version}", prefix: :success
|
|
180
180
|
else
|
|
181
181
|
mod_list.add(mod, enabled: true)
|
|
182
182
|
say "Added #{mod} to mod-list.json", prefix: :success
|
|
183
183
|
end
|
|
184
184
|
end
|
|
185
185
|
end
|
|
186
|
-
|
|
187
|
-
# Download MODs in parallel
|
|
188
|
-
#
|
|
189
|
-
# @param targets [Array<Hash>] Update targets
|
|
190
|
-
# @param jobs [Integer] Number of parallel jobs
|
|
191
|
-
# @return [void]
|
|
192
|
-
private def download_mods(targets, jobs)
|
|
193
|
-
multi_presenter = Progress::MultiPresenter.new(title: "\u{1F4E5}\u{FE0E} Downloads", output: err)
|
|
194
|
-
|
|
195
|
-
pool = Concurrent::FixedThreadPool.new(jobs)
|
|
196
|
-
|
|
197
|
-
futures = targets.map {|target|
|
|
198
|
-
Concurrent::Future.execute(executor: pool) do
|
|
199
|
-
thread_portal = Container[:portal]
|
|
200
|
-
thread_downloader = thread_portal.mod_download_api.downloader
|
|
201
|
-
|
|
202
|
-
presenter = multi_presenter.register(
|
|
203
|
-
target[:mod].name,
|
|
204
|
-
title: target[:latest_release].file_name
|
|
205
|
-
)
|
|
206
|
-
handler = Progress::DownloadHandler.new(presenter)
|
|
207
|
-
|
|
208
|
-
thread_downloader.subscribe(handler)
|
|
209
|
-
thread_portal.download_mod(target[:latest_release], target[:output_path])
|
|
210
|
-
thread_downloader.unsubscribe(handler)
|
|
211
|
-
end
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
futures.each(&:wait!)
|
|
215
|
-
ensure
|
|
216
|
-
pool&.shutdown
|
|
217
|
-
pool&.wait_for_termination
|
|
218
|
-
end
|
|
219
186
|
end
|
|
220
187
|
end
|
|
221
188
|
end
|
|
@@ -6,10 +6,7 @@ module Factorix
|
|
|
6
6
|
module MOD
|
|
7
7
|
# Upload MOD to Factorio MOD Portal (handles both new and update)
|
|
8
8
|
class Upload < Base
|
|
9
|
-
|
|
10
|
-
# # @return [Portal]
|
|
11
|
-
# attr_reader :portal
|
|
12
|
-
include Import[:portal]
|
|
9
|
+
include PortalSupport
|
|
13
10
|
|
|
14
11
|
desc "Upload MOD to Factorio MOD Portal (handles both new and update)"
|
|
15
12
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
# Provides lazy Portal resolution for CLI commands
|
|
7
|
+
#
|
|
8
|
+
# This module defers Portal dependency resolution until first use,
|
|
9
|
+
# allowing configuration to be loaded before cache backends are resolved.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# class Show < Base
|
|
13
|
+
# include PortalSupport
|
|
14
|
+
#
|
|
15
|
+
# def call(mod_name:, **)
|
|
16
|
+
# mod_info = portal.get_mod(mod_name)
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
module PortalSupport
|
|
20
|
+
# Lazily resolve Portal from Container
|
|
21
|
+
#
|
|
22
|
+
# @return [Portal] the portal instance
|
|
23
|
+
private def portal = @portal ||= Container[:portal]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/factorix/container.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "dry/core"
|
|
4
|
+
require "dry/inflector"
|
|
4
5
|
require "dry/logger"
|
|
5
6
|
|
|
6
7
|
module Factorix
|
|
@@ -13,12 +14,28 @@ module Factorix
|
|
|
13
14
|
class Container
|
|
14
15
|
extend Dry::Core::Container::Mixin
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
INFLECTOR = Dry::Inflector.new do |inflections|
|
|
18
|
+
inflections.uncountable("redis")
|
|
19
|
+
end
|
|
20
|
+
private_constant :INFLECTOR
|
|
21
|
+
|
|
22
|
+
# Build a cache instance from configuration.
|
|
23
|
+
#
|
|
24
|
+
# @param cache_type [Symbol] cache type (:download, :api, :info_json)
|
|
25
|
+
# @param config [Dry::Configurable::Config] cache configuration
|
|
26
|
+
# @return [Cache::Base] cache instance
|
|
27
|
+
def self.build_cache(cache_type, config)
|
|
28
|
+
backend = config.backend
|
|
29
|
+
backend_config = config.public_send(backend).to_h
|
|
30
|
+
backend_class = Cache.const_get(INFLECTOR.classify(backend.to_s))
|
|
31
|
+
backend_class.new(cache_type:, ttl: config.ttl, **backend_config)
|
|
32
|
+
end
|
|
33
|
+
private_class_method :build_cache
|
|
34
|
+
|
|
35
|
+
# :downloader is registered with memoize: false to support independent event handlers
|
|
17
36
|
# for each parallel download task (e.g., progress tracking).
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# - :mod_download_api (contains :downloader)
|
|
21
|
-
# - :portal (contains :mod_download_api)
|
|
37
|
+
# MODDownloadAPI resolves :downloader lazily per download call, allowing
|
|
38
|
+
# :mod_download_api and :portal to be safely memoized.
|
|
22
39
|
|
|
23
40
|
# Register runtime detector
|
|
24
41
|
register(:runtime, memoize: true) do
|
|
@@ -48,20 +65,17 @@ module Factorix
|
|
|
48
65
|
|
|
49
66
|
# Register download cache
|
|
50
67
|
register(:download_cache, memoize: true) do
|
|
51
|
-
|
|
52
|
-
Cache::FileSystem.new(c.dir, **c.to_h.except(:dir))
|
|
68
|
+
build_cache(:download, Factorix.config.cache.download)
|
|
53
69
|
end
|
|
54
70
|
|
|
55
71
|
# Register API cache (with compression for JSON responses)
|
|
56
72
|
register(:api_cache, memoize: true) do
|
|
57
|
-
|
|
58
|
-
Cache::FileSystem.new(c.dir, **c.to_h.except(:dir))
|
|
73
|
+
build_cache(:api, Factorix.config.cache.api)
|
|
59
74
|
end
|
|
60
75
|
|
|
61
76
|
# Register info.json cache (for MOD metadata from ZIP files)
|
|
62
77
|
register(:info_json_cache, memoize: true) do
|
|
63
|
-
|
|
64
|
-
Cache::FileSystem.new(c.dir, **c.to_h.except(:dir))
|
|
78
|
+
build_cache(:info_json, Factorix.config.cache.info_json)
|
|
65
79
|
end
|
|
66
80
|
|
|
67
81
|
# Register base HTTP client
|
|
@@ -118,7 +132,7 @@ module Factorix
|
|
|
118
132
|
end
|
|
119
133
|
|
|
120
134
|
# Register MOD Download API client
|
|
121
|
-
register(:mod_download_api, memoize:
|
|
135
|
+
register(:mod_download_api, memoize: true) do
|
|
122
136
|
API::MODDownloadAPI.new
|
|
123
137
|
end
|
|
124
138
|
|
|
@@ -134,7 +148,7 @@ module Factorix
|
|
|
134
148
|
end
|
|
135
149
|
|
|
136
150
|
# Register portal (high-level API wrapper)
|
|
137
|
-
register(:portal, memoize:
|
|
151
|
+
register(:portal, memoize: true) do
|
|
138
152
|
Portal.new
|
|
139
153
|
end
|
|
140
154
|
end
|
data/lib/factorix/errors.rb
CHANGED
|
@@ -48,6 +48,9 @@ module Factorix
|
|
|
48
48
|
class HTTPNotFoundError < HTTPClientError; end
|
|
49
49
|
class HTTPServerError < HTTPError; end
|
|
50
50
|
|
|
51
|
+
# Cache lock timeout errors
|
|
52
|
+
class LockTimeoutError < InfrastructureError; end
|
|
53
|
+
|
|
51
54
|
# Digest verification errors
|
|
52
55
|
class DigestMismatchError < InfrastructureError; end
|
|
53
56
|
|