factorix 0.6.0 → 0.8.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 +36 -0
- data/README.md +3 -0
- data/completion/_factorix.bash +15 -1
- data/completion/_factorix.fish +15 -7
- data/completion/_factorix.zsh +22 -0
- data/exe/factorix +17 -0
- data/lib/factorix/api/game_download_api.rb +154 -0
- data/lib/factorix/api/mod_download_api.rb +10 -5
- data/lib/factorix/api/mod_info.rb +1 -1
- 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.rb +150 -0
- 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/list.rb +3 -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/cli.rb +1 -0
- data/lib/factorix/container.rb +32 -13
- data/lib/factorix/dependency/graph/builder.rb +2 -2
- data/lib/factorix/dependency/graph.rb +2 -2
- data/lib/factorix/dependency/validation_result.rb +3 -3
- data/lib/factorix/errors.rb +3 -0
- data/lib/factorix/http/cache_decorator.rb +14 -7
- data/lib/factorix/http/cached_response.rb +4 -1
- data/lib/factorix/http/client.rb +13 -3
- data/lib/factorix/http/response.rb +4 -1
- data/lib/factorix/http/retry_decorator.rb +11 -0
- data/lib/factorix/info_json.rb +5 -5
- data/lib/factorix/portal.rb +3 -2
- data/lib/factorix/save_file.rb +2 -2
- data/lib/factorix/transfer/downloader.rb +19 -11
- data/lib/factorix/version.rb +1 -1
- data/lib/factorix.rb +46 -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 +27 -2
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
# Download Factorio game files from the official download API
|
|
7
|
+
class Download < Base
|
|
8
|
+
# @!parse
|
|
9
|
+
# # @return [Dry::Logger::Dispatcher]
|
|
10
|
+
# attr_reader :logger
|
|
11
|
+
# # @return [Runtime]
|
|
12
|
+
# attr_reader :runtime
|
|
13
|
+
# # @return [API::GameDownloadAPI]
|
|
14
|
+
# attr_reader :game_download_api
|
|
15
|
+
include Import[:logger, :runtime, :game_download_api]
|
|
16
|
+
|
|
17
|
+
# Platform mapping from Runtime to API platform identifier
|
|
18
|
+
PLATFORM_MAP = {
|
|
19
|
+
"MacOS" => "osx",
|
|
20
|
+
"Linux" => "linux64",
|
|
21
|
+
"Windows" => "win64",
|
|
22
|
+
"WSL" => "win64"
|
|
23
|
+
}.freeze
|
|
24
|
+
private_constant :PLATFORM_MAP
|
|
25
|
+
|
|
26
|
+
desc "Download Factorio game files"
|
|
27
|
+
|
|
28
|
+
argument :version, required: false, default: "latest", desc: "Version (e.g., 2.0.73, latest)"
|
|
29
|
+
|
|
30
|
+
option :build, aliases: ["-b"], default: "alpha", values: API::GameDownloadAPI::BUILDS, desc: "Build type"
|
|
31
|
+
option :platform, aliases: ["-p"], values: API::GameDownloadAPI::PLATFORMS, desc: "Platform (default: auto-detect)"
|
|
32
|
+
option :channel, aliases: ["-c"], default: "stable", values: API::GameDownloadAPI::CHANNELS, desc: "Release channel"
|
|
33
|
+
option :directory, aliases: ["-d"], default: ".", desc: "Download directory"
|
|
34
|
+
option :output, aliases: ["-o"], desc: "Output filename (default: from server)"
|
|
35
|
+
|
|
36
|
+
example [
|
|
37
|
+
" # Download latest stable version (auto-detect platform)",
|
|
38
|
+
"2.0.73 # Download specific version",
|
|
39
|
+
"--build expansion # Download expansion build",
|
|
40
|
+
"--build headless -p linux64 # Download headless server for Linux",
|
|
41
|
+
"--channel experimental # Download experimental release",
|
|
42
|
+
"-o factorio-server.tar.xz # Specify output filename"
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# Execute the download command
|
|
46
|
+
#
|
|
47
|
+
# @param version [String] Version to download
|
|
48
|
+
# @param build [String] Build type
|
|
49
|
+
# @param platform [String, nil] Platform (nil for auto-detect)
|
|
50
|
+
# @param channel [String] Release channel
|
|
51
|
+
# @param directory [String] Download directory
|
|
52
|
+
# @param output [String, nil] Output filename
|
|
53
|
+
# @return [void]
|
|
54
|
+
def call(version: "latest", build: "alpha", platform: nil, channel: "stable", directory: ".", output: nil, **)
|
|
55
|
+
platform ||= detect_platform
|
|
56
|
+
resolved_version = resolve_version(version, channel, build)
|
|
57
|
+
|
|
58
|
+
download_dir = Pathname(directory).expand_path
|
|
59
|
+
raise DirectoryNotFoundError, "Download directory does not exist: #{download_dir}" unless download_dir.exist?
|
|
60
|
+
|
|
61
|
+
filename = output || resolve_filename(resolved_version, build, platform)
|
|
62
|
+
output_path = download_dir / filename
|
|
63
|
+
|
|
64
|
+
say "Downloading Factorio #{resolved_version} (#{build}/#{platform})...", prefix: :info
|
|
65
|
+
|
|
66
|
+
download_game(resolved_version, build, platform, output_path)
|
|
67
|
+
|
|
68
|
+
say "Downloaded to #{output_path}", prefix: :success
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Detect platform from Runtime
|
|
72
|
+
#
|
|
73
|
+
# @return [String] Platform identifier
|
|
74
|
+
private def detect_platform
|
|
75
|
+
runtime_class = runtime.class.name.split("::").last
|
|
76
|
+
platform = PLATFORM_MAP[runtime_class]
|
|
77
|
+
raise UnsupportedPlatformError, "Cannot auto-detect platform for #{runtime_class}" unless platform
|
|
78
|
+
|
|
79
|
+
logger.debug("Auto-detected platform", platform:)
|
|
80
|
+
platform
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Minimum supported major version
|
|
84
|
+
MINIMUM_MAJOR_VERSION = 2
|
|
85
|
+
private_constant :MINIMUM_MAJOR_VERSION
|
|
86
|
+
|
|
87
|
+
# Resolve version, handling "latest" by fetching from API
|
|
88
|
+
#
|
|
89
|
+
# @param version [String] Version or "latest"
|
|
90
|
+
# @param channel [String] Release channel
|
|
91
|
+
# @param build [String] Build type
|
|
92
|
+
# @return [String] Resolved version
|
|
93
|
+
# @raise [InvalidArgumentError] if version is invalid or < 2.0
|
|
94
|
+
private def resolve_version(version, channel, build)
|
|
95
|
+
resolved = if version == "latest"
|
|
96
|
+
v = game_download_api.latest_version(channel:, build:)
|
|
97
|
+
raise InvalidArgumentError, "No #{channel} version available for #{build}" unless v
|
|
98
|
+
|
|
99
|
+
logger.debug("Resolved latest version", channel:, build:, version: v)
|
|
100
|
+
v
|
|
101
|
+
else
|
|
102
|
+
version
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
validate_version!(resolved)
|
|
106
|
+
resolved
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Validate version format and minimum version requirement
|
|
110
|
+
#
|
|
111
|
+
# @param version [String] Version string
|
|
112
|
+
# @return [void]
|
|
113
|
+
# @raise [InvalidArgumentError] if version is invalid or < 2.0
|
|
114
|
+
private def validate_version!(version)
|
|
115
|
+
game_version = GameVersion.from_string(version)
|
|
116
|
+
|
|
117
|
+
return if game_version.major >= MINIMUM_MAJOR_VERSION
|
|
118
|
+
|
|
119
|
+
raise InvalidArgumentError, "Version #{version} is not supported. Minimum version is #{MINIMUM_MAJOR_VERSION}.0.0"
|
|
120
|
+
rescue VersionParseError => e
|
|
121
|
+
raise InvalidArgumentError, "Invalid version format: #{e.message}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Resolve filename by making HEAD request
|
|
125
|
+
#
|
|
126
|
+
# @param version [String] Version
|
|
127
|
+
# @param build [String] Build type
|
|
128
|
+
# @param platform [String] Platform
|
|
129
|
+
# @return [String] Filename
|
|
130
|
+
private def resolve_filename(version, build, platform)
|
|
131
|
+
game_download_api.resolve_filename(version:, build:, platform:)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Download the game with progress tracking
|
|
135
|
+
#
|
|
136
|
+
# @param version [String] Version
|
|
137
|
+
# @param build [String] Build type
|
|
138
|
+
# @param platform [String] Platform
|
|
139
|
+
# @param output_path [Pathname] Output file path
|
|
140
|
+
# @return [void]
|
|
141
|
+
private def download_game(version, build, platform, output_path)
|
|
142
|
+
presenter = Progress::Presenter.new(title: output_path.basename.to_s, output: err)
|
|
143
|
+
handler = Progress::DownloadHandler.new(presenter)
|
|
144
|
+
|
|
145
|
+
game_download_api.download(version:, build:, platform:, output: output_path, handler:)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
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
|
|
|
@@ -147,7 +147,7 @@ module Factorix
|
|
|
147
147
|
enabled = mod_list.exist?(mod) && mod_list.enabled?(mod)
|
|
148
148
|
error = error_map[mod.name]
|
|
149
149
|
|
|
150
|
-
MODInfo
|
|
150
|
+
MODInfo[name: mod.name, version: display_version, enabled:, error:, latest_version: nil]
|
|
151
151
|
}
|
|
152
152
|
end
|
|
153
153
|
|
|
@@ -256,13 +256,13 @@ module Factorix
|
|
|
256
256
|
private def fetch_latest_version_for_mod(info)
|
|
257
257
|
portal_info = mod_portal_api.get_mod(info.name)
|
|
258
258
|
latest = portal_info[:releases]&.map {|r| MODVersion.from_string(r[:version]) }&.max
|
|
259
|
-
MODInfo
|
|
259
|
+
MODInfo[
|
|
260
260
|
name: info.name,
|
|
261
261
|
version: info.version,
|
|
262
262
|
enabled: info.enabled,
|
|
263
263
|
error: info.error,
|
|
264
264
|
latest_version: latest
|
|
265
|
-
|
|
265
|
+
]
|
|
266
266
|
rescue MODNotOnPortalError
|
|
267
267
|
logger.debug("MOD not found on portal", mod: info.name)
|
|
268
268
|
info
|
|
@@ -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/cli.rb
CHANGED
|
@@ -17,6 +17,7 @@ module Factorix
|
|
|
17
17
|
register "man", Commands::Man
|
|
18
18
|
register "launch", Commands::Launch
|
|
19
19
|
register "path", Commands::Path
|
|
20
|
+
register "download", Commands::Download
|
|
20
21
|
register "completion", Commands::Completion
|
|
21
22
|
register "mod check", Commands::MOD::Check
|
|
22
23
|
register "mod list", Commands::MOD::List
|
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,10 +132,15 @@ 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
|
|
|
139
|
+
# Register Game Download API client
|
|
140
|
+
register(:game_download_api, memoize: true) do
|
|
141
|
+
API::GameDownloadAPI.new
|
|
142
|
+
end
|
|
143
|
+
|
|
125
144
|
# Register API credential (for MOD upload/management)
|
|
126
145
|
register(:api_credential, memoize: true) { APICredential.load }
|
|
127
146
|
|
|
@@ -134,7 +153,7 @@ module Factorix
|
|
|
134
153
|
end
|
|
135
154
|
|
|
136
155
|
# Register portal (high-level API wrapper)
|
|
137
|
-
register(:portal, memoize:
|
|
156
|
+
register(:portal, memoize: true) do
|
|
138
157
|
Portal.new
|
|
139
158
|
end
|
|
140
159
|
end
|
|
@@ -53,7 +53,7 @@ module Factorix
|
|
|
53
53
|
version = select_version_for_mod(mod)
|
|
54
54
|
enabled = mod_enabled?(mod)
|
|
55
55
|
|
|
56
|
-
node = Node
|
|
56
|
+
node = Node[mod:, version:, enabled:, installed: true]
|
|
57
57
|
graph.add_node(node)
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -88,7 +88,7 @@ module Factorix
|
|
|
88
88
|
# Expansion MODs can be disabled, so they must be validated
|
|
89
89
|
next if dependency.mod.base?
|
|
90
90
|
|
|
91
|
-
edge = Edge
|
|
91
|
+
edge = Edge[from_mod:, to_mod: dependency.mod, type: dependency.type, version_requirement: dependency.version_requirement]
|
|
92
92
|
graph.add_edge(edge)
|
|
93
93
|
end
|
|
94
94
|
end
|
|
@@ -75,7 +75,7 @@ module Factorix
|
|
|
75
75
|
return
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
node = Node
|
|
78
|
+
node = Node[mod:, version: release.version, enabled: false, installed: false, operation:]
|
|
79
79
|
add_node(node)
|
|
80
80
|
|
|
81
81
|
dependencies = release.info_json[:dependencies] || []
|
|
@@ -85,7 +85,7 @@ module Factorix
|
|
|
85
85
|
dependency = parser.parse(dep_string)
|
|
86
86
|
next if dependency.mod.base?
|
|
87
87
|
|
|
88
|
-
edge = Edge
|
|
88
|
+
edge = Edge[from_mod: mod, to_mod: dependency.mod, type: dependency.type, version_requirement: dependency.version_requirement]
|
|
89
89
|
|
|
90
90
|
add_edge(edge)
|
|
91
91
|
end
|
|
@@ -81,7 +81,7 @@ module Factorix
|
|
|
81
81
|
# @param mod [Factorix::MOD, nil] Related MOD
|
|
82
82
|
# @param dependency [Factorix::MOD, nil] Dependency MOD
|
|
83
83
|
# @return [void]
|
|
84
|
-
def add_error(type:, message:, mod: nil, dependency: nil) = @errors << Error
|
|
84
|
+
def add_error(type:, message:, mod: nil, dependency: nil) = @errors << Error[type:, message:, mod:, dependency:]
|
|
85
85
|
|
|
86
86
|
# Add a warning
|
|
87
87
|
#
|
|
@@ -89,7 +89,7 @@ module Factorix
|
|
|
89
89
|
# @param message [String] Warning message
|
|
90
90
|
# @param mod [Factorix::MOD, nil] Related MOD
|
|
91
91
|
# @return [void]
|
|
92
|
-
def add_warning(type:, message:, mod: nil) = @warnings << Warning
|
|
92
|
+
def add_warning(type:, message:, mod: nil) = @warnings << Warning[type:, message:, mod:]
|
|
93
93
|
|
|
94
94
|
# Add a suggestion
|
|
95
95
|
#
|
|
@@ -97,7 +97,7 @@ module Factorix
|
|
|
97
97
|
# @param mod [Factorix::MOD] Related MOD
|
|
98
98
|
# @param version [Factorix::MODVersion] Suggested version
|
|
99
99
|
# @return [void]
|
|
100
|
-
def add_suggestion(message:, mod:, version:) = @suggestions << Suggestion
|
|
100
|
+
def add_suggestion(message:, mod:, version:) = @suggestions << Suggestion[message:, mod:, version:]
|
|
101
101
|
|
|
102
102
|
# Get all errors
|
|
103
103
|
#
|
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
|
|