factorix 0.5.1 → 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 +45 -0
- data/README.md +1 -1
- data/exe/factorix +17 -0
- data/lib/factorix/api/mod_download_api.rb +11 -6
- data/lib/factorix/api/mod_info.rb +2 -2
- data/lib/factorix/api/mod_management_api.rb +1 -1
- data/lib/factorix/api/mod_portal_api.rb +6 -49
- data/lib/factorix/api_credential.rb +1 -1
- 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/backup_support.rb +1 -1
- data/lib/factorix/cli/commands/base.rb +3 -3
- data/lib/factorix/cli/commands/cache/evict.rb +19 -24
- data/lib/factorix/cli/commands/cache/stat.rb +66 -67
- data/lib/factorix/cli/commands/command_wrapper.rb +5 -5
- data/lib/factorix/cli/commands/completion.rb +1 -2
- data/lib/factorix/cli/commands/confirmable.rb +1 -1
- data/lib/factorix/cli/commands/download_support.rb +2 -7
- data/lib/factorix/cli/commands/mod/check.rb +1 -1
- data/lib/factorix/cli/commands/mod/disable.rb +1 -1
- data/lib/factorix/cli/commands/mod/download.rb +7 -7
- data/lib/factorix/cli/commands/mod/edit.rb +10 -13
- data/lib/factorix/cli/commands/mod/enable.rb +1 -1
- data/lib/factorix/cli/commands/mod/image/add.rb +3 -6
- data/lib/factorix/cli/commands/mod/image/edit.rb +2 -5
- data/lib/factorix/cli/commands/mod/image/list.rb +5 -8
- data/lib/factorix/cli/commands/mod/install.rb +7 -7
- data/lib/factorix/cli/commands/mod/list.rb +7 -7
- data/lib/factorix/cli/commands/mod/search.rb +13 -12
- data/lib/factorix/cli/commands/mod/settings/dump.rb +3 -3
- data/lib/factorix/cli/commands/mod/settings/restore.rb +2 -2
- data/lib/factorix/cli/commands/mod/show.rb +22 -23
- data/lib/factorix/cli/commands/mod/sync.rb +8 -8
- data/lib/factorix/cli/commands/mod/uninstall.rb +1 -1
- data/lib/factorix/cli/commands/mod/update.rb +11 -43
- data/lib/factorix/cli/commands/mod/upload.rb +7 -10
- data/lib/factorix/cli/commands/path.rb +2 -2
- data/lib/factorix/cli/commands/portal_support.rb +27 -0
- data/lib/factorix/cli/commands/version.rb +1 -1
- data/lib/factorix/container.rb +155 -0
- data/lib/factorix/dependency/parser.rb +1 -1
- data/lib/factorix/errors.rb +3 -0
- data/lib/factorix/http/cache_decorator.rb +5 -5
- data/lib/factorix/http/client.rb +3 -3
- data/lib/factorix/info_json.rb +7 -7
- data/lib/factorix/mod_list.rb +2 -2
- data/lib/factorix/mod_settings.rb +2 -2
- data/lib/factorix/portal.rb +3 -2
- data/lib/factorix/runtime/user_configurable.rb +9 -9
- data/lib/factorix/service_credential.rb +3 -3
- data/lib/factorix/transfer/downloader.rb +19 -11
- data/lib/factorix/version.rb +1 -1
- data/lib/factorix.rb +110 -1
- 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/container.rbs +15 -0
- data/sig/factorix/errors.rbs +3 -0
- data/sig/factorix/portal.rbs +1 -1
- data/sig/factorix.rbs +99 -0
- metadata +27 -4
- data/lib/factorix/application.rb +0 -218
- data/sig/factorix/application.rbs +0 -86
|
@@ -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
|
|
|
@@ -29,11 +28,11 @@ module Factorix
|
|
|
29
28
|
argument :mod_names, type: :array, required: false, default: [], desc: "MOD names to search"
|
|
30
29
|
|
|
31
30
|
option :hide_deprecated, type: :boolean, default: true, desc: "Hide deprecated MOD(s)"
|
|
32
|
-
option :page,
|
|
33
|
-
option :page_size,
|
|
34
|
-
option :sort,
|
|
35
|
-
option :sort_order,
|
|
36
|
-
option :version,
|
|
31
|
+
option :page, default: "1", desc: "Page number"
|
|
32
|
+
option :page_size, default: "25", desc: "Results per page (max 500)"
|
|
33
|
+
option :sort, values: %w[name created_at updated_at], desc: "Sort field"
|
|
34
|
+
option :sort_order, values: %w[asc desc], desc: "Sort order"
|
|
35
|
+
option :version, desc: "Filter by Factorio version (default: installed version)"
|
|
37
36
|
option :json, type: :flag, default: false, desc: "Output in JSON format"
|
|
38
37
|
|
|
39
38
|
# Execute the search command
|
|
@@ -47,7 +46,9 @@ module Factorix
|
|
|
47
46
|
# @param version [String, nil] Factorio version filter
|
|
48
47
|
# @param json [Boolean] Output in JSON format
|
|
49
48
|
# @return [void]
|
|
50
|
-
def call(mod_names: [], hide_deprecated: true, page: 1, page_size: 25, sort: nil, sort_order: nil, version: nil, json: false, **)
|
|
49
|
+
def call(mod_names: [], hide_deprecated: true, page: "1", page_size: "25", sort: nil, sort_order: nil, version: nil, json: false, **)
|
|
50
|
+
page = Integer(page)
|
|
51
|
+
page_size = Integer(page_size)
|
|
51
52
|
version ||= default_factorio_version
|
|
52
53
|
|
|
53
54
|
mods = portal.list_mods(*mod_names, hide_deprecated: hide_deprecated || nil, page:, page_size:, sort:, sort_order:, version:)
|
|
@@ -60,7 +61,7 @@ module Factorix
|
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
private def output_json(mods)
|
|
63
|
-
puts JSON.pretty_generate(mods.map {|mod| mod_to_hash(mod) })
|
|
64
|
+
out.puts JSON.pretty_generate(mods.map {|mod| mod_to_hash(mod) })
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
private def mod_to_hash(mod)
|
|
@@ -99,10 +100,10 @@ module Factorix
|
|
|
99
100
|
headers = %w[NAME TITLE CATEGORY OWNER LATEST]
|
|
100
101
|
widths = headers.map.with_index {|h, i| [h.length, *rows.map {|r| r[i].to_s.length }].max }
|
|
101
102
|
|
|
102
|
-
puts format_table_row(headers, widths)
|
|
103
|
+
out.puts format_table_row(headers, widths)
|
|
103
104
|
|
|
104
105
|
rows.each do |row|
|
|
105
|
-
puts format_table_row(row, widths)
|
|
106
|
+
out.puts format_table_row(row, widths)
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
say "#{mods.size} MOD(s) found", prefix: :info
|
|
@@ -22,8 +22,8 @@ module Factorix
|
|
|
22
22
|
"/path/to/mod-settings.dat -o out.json # Dump specific file"
|
|
23
23
|
]
|
|
24
24
|
|
|
25
|
-
argument :settings_file,
|
|
26
|
-
option :output,
|
|
25
|
+
argument :settings_file, required: false, desc: "Path to mod-settings.dat file"
|
|
26
|
+
option :output, aliases: ["-o"], desc: "Output file path"
|
|
27
27
|
|
|
28
28
|
# Execute the dump command
|
|
29
29
|
#
|
|
@@ -43,7 +43,7 @@ module Factorix
|
|
|
43
43
|
if output
|
|
44
44
|
Pathname(output).write(output_string)
|
|
45
45
|
else
|
|
46
|
-
puts output_string
|
|
46
|
+
out.puts output_string
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -24,8 +24,8 @@ module Factorix
|
|
|
24
24
|
" # Restore from stdin"
|
|
25
25
|
]
|
|
26
26
|
|
|
27
|
-
argument :settings_file,
|
|
28
|
-
option :input,
|
|
27
|
+
argument :settings_file, required: false, desc: "Path to mod-settings.dat file to write"
|
|
28
|
+
option :input, aliases: ["-i"], desc: "Input file path"
|
|
29
29
|
|
|
30
30
|
# Execute the restore command
|
|
31
31
|
#
|
|
@@ -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
|
|
|
@@ -32,7 +31,7 @@ module Factorix
|
|
|
32
31
|
"some-mod # Show details for some-mod"
|
|
33
32
|
]
|
|
34
33
|
|
|
35
|
-
argument :mod_name,
|
|
34
|
+
argument :mod_name, required: true, desc: "MOD name to show"
|
|
36
35
|
|
|
37
36
|
# Execute the show command
|
|
38
37
|
#
|
|
@@ -75,10 +74,10 @@ module Factorix
|
|
|
75
74
|
end
|
|
76
75
|
|
|
77
76
|
private def display_header(mod_info)
|
|
78
|
-
puts TITLE_STYLE[mod_info.title]
|
|
79
|
-
puts
|
|
80
|
-
puts mod_info.summary unless mod_info.summary.empty?
|
|
81
|
-
puts
|
|
77
|
+
out.puts TITLE_STYLE[mod_info.title]
|
|
78
|
+
out.puts
|
|
79
|
+
out.puts mod_info.summary unless mod_info.summary.empty?
|
|
80
|
+
out.puts
|
|
82
81
|
end
|
|
83
82
|
|
|
84
83
|
private def display_basic_info(mod_info, local_status)
|
|
@@ -103,9 +102,9 @@ module Factorix
|
|
|
103
102
|
|
|
104
103
|
max_label_width = rows.map {|label, _| label.length }.max
|
|
105
104
|
rows.each do |label, value|
|
|
106
|
-
puts "#{label.ljust(max_label_width)} #{value}"
|
|
105
|
+
out.puts "#{label.ljust(max_label_width)} #{value}"
|
|
107
106
|
end
|
|
108
|
-
puts
|
|
107
|
+
out.puts
|
|
109
108
|
end
|
|
110
109
|
|
|
111
110
|
private def format_status(local_status)
|
|
@@ -123,18 +122,18 @@ module Factorix
|
|
|
123
122
|
end
|
|
124
123
|
|
|
125
124
|
private def display_links(mod_info)
|
|
126
|
-
puts HEADER_STYLE["Links"]
|
|
127
|
-
puts " MOD Portal: https://mods.factorio.com/mod/#{mod_info.name}"
|
|
125
|
+
out.puts HEADER_STYLE["Links"]
|
|
126
|
+
out.puts " MOD Portal: https://mods.factorio.com/mod/#{mod_info.name}"
|
|
128
127
|
|
|
129
128
|
if mod_info.detail
|
|
130
129
|
if mod_info.detail.source_url
|
|
131
|
-
puts " Source: #{mod_info.detail.source_url}"
|
|
130
|
+
out.puts " Source: #{mod_info.detail.source_url}"
|
|
132
131
|
end
|
|
133
132
|
if mod_info.detail.homepage
|
|
134
|
-
puts " Homepage: #{mod_info.detail.homepage}"
|
|
133
|
+
out.puts " Homepage: #{mod_info.detail.homepage}"
|
|
135
134
|
end
|
|
136
135
|
end
|
|
137
|
-
puts
|
|
136
|
+
out.puts
|
|
138
137
|
end
|
|
139
138
|
|
|
140
139
|
private def display_dependencies(mod_info)
|
|
@@ -149,16 +148,16 @@ module Factorix
|
|
|
149
148
|
optional = parsed.select {|d| d[:type] == :optional }
|
|
150
149
|
|
|
151
150
|
unless required.empty?
|
|
152
|
-
puts HEADER_STYLE["Dependencies"]
|
|
151
|
+
out.puts HEADER_STYLE["Dependencies"]
|
|
153
152
|
required.each {|dep| display_dependency(dep) }
|
|
154
|
-
puts
|
|
153
|
+
out.puts
|
|
155
154
|
end
|
|
156
155
|
|
|
157
156
|
return if optional.empty?
|
|
158
157
|
|
|
159
|
-
puts HEADER_STYLE["Optional Dependencies"]
|
|
158
|
+
out.puts HEADER_STYLE["Optional Dependencies"]
|
|
160
159
|
optional.each {|dep| display_dependency(dep) }
|
|
161
|
-
puts
|
|
160
|
+
out.puts
|
|
162
161
|
end
|
|
163
162
|
|
|
164
163
|
private def parse_dependency(dep_str)
|
|
@@ -178,7 +177,7 @@ module Factorix
|
|
|
178
177
|
end
|
|
179
178
|
|
|
180
179
|
private def display_dependency(dep)
|
|
181
|
-
puts " #{dep[:spec]}"
|
|
180
|
+
out.puts " #{dep[:spec]}"
|
|
182
181
|
end
|
|
183
182
|
|
|
184
183
|
private def display_incompatibilities(mod_info)
|
|
@@ -191,9 +190,9 @@ module Factorix
|
|
|
191
190
|
|
|
192
191
|
return if incompatible.empty?
|
|
193
192
|
|
|
194
|
-
puts HEADER_STYLE["Incompatibilities"]
|
|
195
|
-
incompatible.each {|dep| puts " #{INCOMPATIBLE_MOD_STYLE[dep[:spec]]}" }
|
|
196
|
-
puts
|
|
193
|
+
out.puts HEADER_STYLE["Incompatibilities"]
|
|
194
|
+
incompatible.each {|dep| out.puts " #{INCOMPATIBLE_MOD_STYLE[dep[:spec]]}" }
|
|
195
|
+
out.puts
|
|
197
196
|
end
|
|
198
197
|
end
|
|
199
198
|
end
|
|
@@ -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
|
|
|
@@ -31,15 +30,16 @@ module Factorix
|
|
|
31
30
|
"-j 8 save.zip # Use 8 parallel downloads"
|
|
32
31
|
]
|
|
33
32
|
|
|
34
|
-
argument :save_file,
|
|
35
|
-
option :jobs,
|
|
33
|
+
argument :save_file, required: true, desc: "Path to Factorio save file (.zip)"
|
|
34
|
+
option :jobs, aliases: ["-j"], default: "4", desc: "Number of parallel downloads"
|
|
36
35
|
|
|
37
36
|
# Execute the sync command
|
|
38
37
|
#
|
|
39
38
|
# @param save_file [String] Path to save file
|
|
40
39
|
# @param jobs [Integer] Number of parallel downloads
|
|
41
40
|
# @return [void]
|
|
42
|
-
def call(save_file:, jobs: 4, **)
|
|
41
|
+
def call(save_file:, jobs: "4", **)
|
|
42
|
+
jobs = Integer(jobs)
|
|
43
43
|
# Load save file
|
|
44
44
|
say "Loading save file: #{save_file}", prefix: :info
|
|
45
45
|
save_data = SaveFile.load(Pathname(save_file))
|
|
@@ -47,7 +47,7 @@ module Factorix
|
|
|
47
47
|
|
|
48
48
|
# Load current state
|
|
49
49
|
mod_list = MODList.load
|
|
50
|
-
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output:
|
|
50
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: err)
|
|
51
51
|
handler = Progress::ScanHandler.new(presenter)
|
|
52
52
|
installed_mods = InstalledMOD.all(handler:)
|
|
53
53
|
graph = Dependency::Graph::Builder.build(installed_mods:, mod_list:)
|
|
@@ -109,7 +109,7 @@ module Factorix
|
|
|
109
109
|
# @return [Array<Hash>] Installation targets with MOD info and releases
|
|
110
110
|
private def plan_installation(mods_to_install, graph, jobs)
|
|
111
111
|
# Create progress presenter for info fetching
|
|
112
|
-
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output:
|
|
112
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output: err)
|
|
113
113
|
|
|
114
114
|
# Fetch info for MODs to install
|
|
115
115
|
target_infos = fetch_target_mod_info(mods_to_install, jobs, presenter)
|
|
@@ -45,7 +45,7 @@ module Factorix
|
|
|
45
45
|
|
|
46
46
|
# Load current state (without validation to allow fixing issues)
|
|
47
47
|
mod_list = MODList.load
|
|
48
|
-
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output:
|
|
48
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: err)
|
|
49
49
|
handler = Progress::ScanHandler.new(presenter)
|
|
50
50
|
installed_mods = InstalledMOD.all(handler:)
|
|
51
51
|
graph = Dependency::Graph::Builder.build(installed_mods:, mod_list:)
|
|
@@ -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
|
|
|
@@ -32,15 +32,16 @@ module Factorix
|
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
argument :mod_names, type: :array, required: false, desc: "MOD names to update (all if not specified)"
|
|
35
|
-
option :jobs,
|
|
35
|
+
option :jobs, aliases: ["-j"], default: "4", desc: "Number of parallel downloads"
|
|
36
36
|
|
|
37
37
|
# Execute the update command
|
|
38
38
|
#
|
|
39
39
|
# @param mod_names [Array<String>] MOD names to update
|
|
40
40
|
# @param jobs [Integer] Number of parallel downloads
|
|
41
41
|
# @return [void]
|
|
42
|
-
def call(mod_names: [], jobs: 4, **)
|
|
43
|
-
|
|
42
|
+
def call(mod_names: [], jobs: "4", **)
|
|
43
|
+
jobs = Integer(jobs)
|
|
44
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: err)
|
|
44
45
|
handler = Progress::ScanHandler.new(presenter)
|
|
45
46
|
installed_mods = InstalledMOD.all(handler:)
|
|
46
47
|
mod_list = MODList.load
|
|
@@ -98,7 +99,7 @@ module Factorix
|
|
|
98
99
|
# @param jobs [Integer] Number of parallel jobs
|
|
99
100
|
# @return [Array<Hash>] Update targets with current and latest versions
|
|
100
101
|
private def find_update_targets(target_mods, installed_mods, jobs)
|
|
101
|
-
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Checking for updates", output:
|
|
102
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Checking for updates", output: err)
|
|
102
103
|
presenter.start(total: target_mods.size)
|
|
103
104
|
|
|
104
105
|
pool = Concurrent::FixedThreadPool.new(jobs)
|
|
@@ -140,7 +141,7 @@ module Factorix
|
|
|
140
141
|
mod:,
|
|
141
142
|
mod_info:,
|
|
142
143
|
current_version:,
|
|
143
|
-
latest_release
|
|
144
|
+
release: latest_release,
|
|
144
145
|
output_path: runtime.mod_dir / latest_release.file_name
|
|
145
146
|
}
|
|
146
147
|
rescue MODNotOnPortalError
|
|
@@ -155,7 +156,7 @@ module Factorix
|
|
|
155
156
|
private def show_plan(targets)
|
|
156
157
|
say "Planning to update #{targets.size} MOD(s):", prefix: :info
|
|
157
158
|
targets.each do |target|
|
|
158
|
-
say " - #{target[:mod]}: #{target[:current_version]} -> #{target[:
|
|
159
|
+
say " - #{target[:mod]}: #{target[:current_version]} -> #{target[:release].version}"
|
|
159
160
|
end
|
|
160
161
|
end
|
|
161
162
|
|
|
@@ -175,46 +176,13 @@ module Factorix
|
|
|
175
176
|
current_enabled = mod_list.enabled?(mod)
|
|
176
177
|
mod_list.remove(mod)
|
|
177
178
|
mod_list.add(mod, enabled: current_enabled)
|
|
178
|
-
say "Updated #{mod} to #{target[:
|
|
179
|
+
say "Updated #{mod} to #{target[:release].version}", prefix: :success
|
|
179
180
|
else
|
|
180
181
|
mod_list.add(mod, enabled: true)
|
|
181
182
|
say "Added #{mod} to mod-list.json", prefix: :success
|
|
182
183
|
end
|
|
183
184
|
end
|
|
184
185
|
end
|
|
185
|
-
|
|
186
|
-
# Download MODs in parallel
|
|
187
|
-
#
|
|
188
|
-
# @param targets [Array<Hash>] Update targets
|
|
189
|
-
# @param jobs [Integer] Number of parallel jobs
|
|
190
|
-
# @return [void]
|
|
191
|
-
private def download_mods(targets, jobs)
|
|
192
|
-
multi_presenter = Progress::MultiPresenter.new(title: "\u{1F4E5}\u{FE0E} Downloads")
|
|
193
|
-
|
|
194
|
-
pool = Concurrent::FixedThreadPool.new(jobs)
|
|
195
|
-
|
|
196
|
-
futures = targets.map {|target|
|
|
197
|
-
Concurrent::Future.execute(executor: pool) do
|
|
198
|
-
thread_portal = Application[:portal]
|
|
199
|
-
thread_downloader = thread_portal.mod_download_api.downloader
|
|
200
|
-
|
|
201
|
-
presenter = multi_presenter.register(
|
|
202
|
-
target[:mod].name,
|
|
203
|
-
title: target[:latest_release].file_name
|
|
204
|
-
)
|
|
205
|
-
handler = Progress::DownloadHandler.new(presenter)
|
|
206
|
-
|
|
207
|
-
thread_downloader.subscribe(handler)
|
|
208
|
-
thread_portal.download_mod(target[:latest_release], target[:output_path])
|
|
209
|
-
thread_downloader.unsubscribe(handler)
|
|
210
|
-
end
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
futures.each(&:wait!)
|
|
214
|
-
ensure
|
|
215
|
-
pool&.shutdown
|
|
216
|
-
pool&.wait_for_termination
|
|
217
|
-
end
|
|
218
186
|
end
|
|
219
187
|
end
|
|
220
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
|
|
|
@@ -18,11 +15,11 @@ module Factorix
|
|
|
18
15
|
"my-mod_1.0.0.zip --category automation # Upload with category"
|
|
19
16
|
]
|
|
20
17
|
|
|
21
|
-
argument :file,
|
|
22
|
-
option :description,
|
|
23
|
-
option :category,
|
|
24
|
-
option :license,
|
|
25
|
-
option :source_url,
|
|
18
|
+
argument :file, required: true, desc: "Path to MOD zip file"
|
|
19
|
+
option :description, desc: "Markdown description"
|
|
20
|
+
option :category, desc: "MOD category"
|
|
21
|
+
option :license, desc: "License identifier"
|
|
22
|
+
option :source_url, desc: "Repository URL"
|
|
26
23
|
|
|
27
24
|
# Execute the upload command
|
|
28
25
|
#
|
|
@@ -44,7 +41,7 @@ module Factorix
|
|
|
44
41
|
mod_name = extract_mod_name(file_path)
|
|
45
42
|
metadata = build_metadata(description:, category:, license:, source_url:)
|
|
46
43
|
|
|
47
|
-
presenter = Progress::Presenter.new(title: "\u{1F4E4} Uploading #{file_path.basename}", output:
|
|
44
|
+
presenter = Progress::Presenter.new(title: "\u{1F4E4} Uploading #{file_path.basename}", output: err)
|
|
48
45
|
|
|
49
46
|
uploader = portal.mod_management_api.uploader
|
|
50
47
|
handler = Progress::UploadHandler.new(presenter)
|
|
@@ -61,7 +61,7 @@ module Factorix
|
|
|
61
61
|
result = PATH_TYPES.transform_values {|method_name| runtime.public_send(method_name).to_s }
|
|
62
62
|
|
|
63
63
|
if json
|
|
64
|
-
puts JSON.pretty_generate(result)
|
|
64
|
+
out.puts JSON.pretty_generate(result)
|
|
65
65
|
else
|
|
66
66
|
output_table(result)
|
|
67
67
|
end
|
|
@@ -70,7 +70,7 @@ module Factorix
|
|
|
70
70
|
private def output_table(result)
|
|
71
71
|
key_width = result.keys.map(&:length).max
|
|
72
72
|
result.each do |key, value|
|
|
73
|
-
puts "%-#{key_width}s %s" % [key, value]
|
|
73
|
+
out.puts "%-#{key_width}s %s" % [key, value]
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
end
|
|
@@ -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
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/core"
|
|
4
|
+
require "dry/inflector"
|
|
5
|
+
require "dry/logger"
|
|
6
|
+
|
|
7
|
+
module Factorix
|
|
8
|
+
# DI container for dependency injection
|
|
9
|
+
#
|
|
10
|
+
# Provides dependency injection container using dry-core's Container.
|
|
11
|
+
#
|
|
12
|
+
# @example Resolve dependencies
|
|
13
|
+
# runtime = Factorix::Container[:runtime]
|
|
14
|
+
class Container
|
|
15
|
+
extend Dry::Core::Container::Mixin
|
|
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
|
|
36
|
+
# for each parallel download task (e.g., progress tracking).
|
|
37
|
+
# MODDownloadAPI resolves :downloader lazily per download call, allowing
|
|
38
|
+
# :mod_download_api and :portal to be safely memoized.
|
|
39
|
+
|
|
40
|
+
# Register runtime detector
|
|
41
|
+
register(:runtime, memoize: true) do
|
|
42
|
+
Runtime.detect
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Register logger
|
|
46
|
+
register(:logger, memoize: true) do
|
|
47
|
+
runtime = resolve(:runtime)
|
|
48
|
+
log_path = runtime.factorix_log_path
|
|
49
|
+
|
|
50
|
+
# Ensure log directory exists
|
|
51
|
+
log_path.dirname.mkpath unless log_path.dirname.exist?
|
|
52
|
+
|
|
53
|
+
# Create logger with file backend
|
|
54
|
+
# Dispatcher level set to DEBUG to allow all messages through
|
|
55
|
+
# Backend controls filtering based on --log-level option
|
|
56
|
+
Dry.Logger(:factorix, level: :debug) do |dispatcher|
|
|
57
|
+
dispatcher.add_backend(level: Factorix.config.log_level, stream: log_path.to_s, template: "[%<time>s] %<severity>s: %<message>s %<payload>s")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Register retry strategy for network operations
|
|
62
|
+
register(:retry_strategy, memoize: true) do
|
|
63
|
+
HTTP::RetryStrategy.new
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Register download cache
|
|
67
|
+
register(:download_cache, memoize: true) do
|
|
68
|
+
build_cache(:download, Factorix.config.cache.download)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Register API cache (with compression for JSON responses)
|
|
72
|
+
register(:api_cache, memoize: true) do
|
|
73
|
+
build_cache(:api, Factorix.config.cache.api)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Register info.json cache (for MOD metadata from ZIP files)
|
|
77
|
+
register(:info_json_cache, memoize: true) do
|
|
78
|
+
build_cache(:info_json, Factorix.config.cache.info_json)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Register base HTTP client
|
|
82
|
+
register(:http_client, memoize: true) do
|
|
83
|
+
HTTP::Client.new(masked_params: %w[username token secure])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Register decorated HTTP client for downloads (with retry only)
|
|
87
|
+
# Note: Caching is handled by Downloader, not at HTTP client level
|
|
88
|
+
register(:download_http_client, memoize: true) do
|
|
89
|
+
client = resolve(:http_client)
|
|
90
|
+
retry_strategy = resolve(:retry_strategy)
|
|
91
|
+
|
|
92
|
+
# Decorate: Client -> Retry (no cache, handled by Downloader)
|
|
93
|
+
HTTP::RetryDecorator.new(client:, retry_strategy:)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Register decorated HTTP client for API calls (with retry + cache)
|
|
97
|
+
register(:api_http_client, memoize: true) do
|
|
98
|
+
client = resolve(:http_client)
|
|
99
|
+
api_cache = resolve(:api_cache)
|
|
100
|
+
retry_strategy = resolve(:retry_strategy)
|
|
101
|
+
|
|
102
|
+
# Decorate: Client -> Cache -> Retry
|
|
103
|
+
cached = HTTP::CacheDecorator.new(client:, cache: api_cache)
|
|
104
|
+
HTTP::RetryDecorator.new(client: cached, retry_strategy:)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Register decorated HTTP client for uploads (with retry only, no cache)
|
|
108
|
+
register(:upload_http_client, memoize: true) do
|
|
109
|
+
client = resolve(:http_client)
|
|
110
|
+
retry_strategy = resolve(:retry_strategy)
|
|
111
|
+
|
|
112
|
+
# Decorate: Client -> Retry (no cache for uploads)
|
|
113
|
+
HTTP::RetryDecorator.new(client:, retry_strategy:)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Register downloader
|
|
117
|
+
register(:downloader, memoize: false) do
|
|
118
|
+
Transfer::Downloader.new
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Register uploader
|
|
122
|
+
register(:uploader, memoize: true) do
|
|
123
|
+
Transfer::Uploader.new
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Register service credential
|
|
127
|
+
register(:service_credential, memoize: true) { ServiceCredential.load }
|
|
128
|
+
|
|
129
|
+
# Register MOD Portal API client
|
|
130
|
+
register(:mod_portal_api, memoize: true) do
|
|
131
|
+
API::MODPortalAPI.new
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Register MOD Download API client
|
|
135
|
+
register(:mod_download_api, memoize: true) do
|
|
136
|
+
API::MODDownloadAPI.new
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Register API credential (for MOD upload/management)
|
|
140
|
+
register(:api_credential, memoize: true) { APICredential.load }
|
|
141
|
+
|
|
142
|
+
# Register MOD Management API client
|
|
143
|
+
register(:mod_management_api, memoize: true) do
|
|
144
|
+
api = API::MODManagementAPI.new
|
|
145
|
+
# Subscribe mod_portal_api to invalidate cache when MOD is changed on portal
|
|
146
|
+
api.subscribe(resolve(:mod_portal_api))
|
|
147
|
+
api
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Register portal (high-level API wrapper)
|
|
151
|
+
register(:portal, memoize: true) do
|
|
152
|
+
Portal.new
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -128,7 +128,7 @@ module Factorix
|
|
|
128
128
|
MODVersionRequirement[operator:, version:]
|
|
129
129
|
rescue VersionParseError => e
|
|
130
130
|
# Skip version requirements with out-of-range version components
|
|
131
|
-
|
|
131
|
+
Container[:logger].warn("Skipping version requirement '#{version_string}': #{e.message}")
|
|
132
132
|
nil
|
|
133
133
|
end
|
|
134
134
|
|
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
|
|