mmi 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,55 +1,40 @@
1
1
  require 'fileutils'
2
2
  require 'nokogiri'
3
+ require 'open-uri'
3
4
 
4
- require 'mmi/cached_download'
5
- require 'mmi/option_attributes'
5
+ require 'mmi/install_utils'
6
+ require 'mmi/constants'
7
+ require 'mmi/content_hash/sha512'
8
+ require 'mmi/property_attributes'
6
9
 
7
10
  module Mmi
8
11
  module Modloader
9
12
  class Fabric
10
- include Mmi::OptionAttributes
13
+ prepend Mmi::PropertyAttributes
11
14
 
12
- opt_accessor :version
13
- opt_accessor :install_type
14
- opt_accessor :mcversion, 'minecraft_version'
15
- opt_accessor(:install_dir ) { Mmi.minecraft_dir }
16
- opt_accessor(:download_mc, 'download_minecraft') { false }
15
+ property :version
16
+ property :install_type, validate: :validate_install_type
17
+ property :minecraft_version
18
+ property :install_dir, default: Mmi::Constants.minecraft_dir
19
+ property :download_minecraft, default: false, validate: :validate_download_minecraft
17
20
 
18
- def initialize(options)
19
- @options = options
20
-
21
- parse!
21
+ def self.allowed_install_types
22
+ %w[
23
+ client
24
+ server
25
+ ]
22
26
  end
23
27
 
24
- def parse!
25
- if self.version
26
- if self.install_type
27
- if allowed_install_types.include?(self.install_type)
28
- if self.mcversion
29
- if [true, false].include?(self.download_mc)
30
- # Pass.
31
- else
32
- raise Mmi::InvalidAttributeError, %Q(Invalid "modloader.download_minecraft". Expecting true or false, got #{self.download_mc.inspect}.)
33
- end
34
- else
35
- raise Mmi::MissingAttributeError, 'Missing "modloader.minecraft_version".'
36
- end
37
- else
38
- raise Mmi::InvalidAttributeError, %Q(Invalid "modloader.install_type". Expecting "client" or "server", got #{self.install_type.inspect}.)
39
- end
40
- else
41
- raise Mmi::MissingAttributeError, 'Missing "modloader.install_type".'
42
- end
43
- else
44
- raise Mmi::MissingAttributeError, 'Missing "modloader.version".'
28
+ def self.validate_install_type(value, errors)
29
+ if !allowed_install_types.include?(value)
30
+ errors << %Q{modloader "install_type" must be one of #{allowed_install_types.map(&:inspect).join(', ')}}
45
31
  end
46
32
  end
47
33
 
48
- def allowed_install_types
49
- %w[
50
- client
51
- server
52
- ]
34
+ def self.validate_download_minecraft(value, errors)
35
+ if ![true, false].include?(value)
36
+ errors << 'modloader "download_minecraft" must be true or false'
37
+ end
53
38
  end
54
39
 
55
40
  def base_uri
@@ -65,7 +50,7 @@ module Mmi
65
50
  end
66
51
 
67
52
  def metadata_path
68
- File.join(Mmi.cache_dir, 'fabric_maven_metadata.xml')
53
+ File.join(Mmi::Constants.cache_dir, 'fabric_maven_metadata.xml')
69
54
  end
70
55
 
71
56
  def installer_uri
@@ -77,7 +62,7 @@ module Mmi
77
62
  end
78
63
 
79
64
  def installer_path
80
- File.join(Mmi.cache_dir, "fabric-installer-#{self.version}.jar")
65
+ File.join(Mmi::Constants.cache_dir, "fabric-installer-#{self.version}.jar")
81
66
  end
82
67
 
83
68
  def absolute_install_dir
@@ -85,19 +70,25 @@ module Mmi
85
70
  end
86
71
 
87
72
  def download_installer
88
- Mmi.info "Downloading fabric-installer version #{self.version.inspect}."
73
+ installer_hash = Mmi::ContentHash::Sha512.new(URI.parse(installer_sha512sum_uri).read)
89
74
 
90
- begin
91
- Mmi::CachedDownload.download_cached(installer_uri, installer_path, sha512_uri: installer_sha512sum_uri)
92
- rescue OpenURI::HTTPError => e
93
- Mmi.fail! %Q(Error when requesting fabric installer. Maybe "modloader.version" == #{version.inspect} is invalid.\n#{e.inspect})
75
+ if !File.exist?(installer_path) || !installer_hash.match_file?(installer_path)
76
+ Mmi.info "Downloading fabric-installer version #{self.version.inspect}."
77
+
78
+ begin
79
+ Mmi::InstallUtils.download_to_file(installer_uri, installer_path, installer_hash)
80
+ rescue OpenURI::HTTPError => e
81
+ Mmi.fail! %Q{Error when requesting fabric installer. Maybe "modloader.version" == #{version.inspect} is invalid.\n#{e.inspect}}
82
+ end
83
+ else
84
+ Mmi.info "Using cached fabric-installer version #{self.version.inspect}."
94
85
  end
95
86
  end
96
87
 
97
88
  def run_installer
98
89
  FileUtils.mkdir_p(absolute_install_dir)
99
90
 
100
- if system('java', '-jar', installer_path, self.install_type, '-dir', absolute_install_dir, '-noprofile', '-mcversion', self.mcversion, self.download_mc ? '-downloadMinecraft' : '')
91
+ if system('java', '-jar', installer_path, self.install_type, '-dir', absolute_install_dir, '-noprofile', '-mcversion', self.minecraft_version, self.download_minecraft ? '-downloadMinecraft' : '')
101
92
  # Pass.
102
93
  else
103
94
  Mmi.fail! 'Failed to install Fabric modloader.'
@@ -111,7 +102,7 @@ module Mmi
111
102
 
112
103
  def available_versions
113
104
  begin
114
- Mmi::CachedDownload.download_cached(metadata_uri, metadata_path, sha512_uri: metadata_sha512sum_uri)
105
+ Mmi::InstallUtils.download_to_file(metadata_uri, metadata_path, Mmi::ContentHash::Sha512.new(URI.parse(metadata_sha512sum_uri).read))
115
106
  rescue OpenURI::HTTPError => e
116
107
  Mmi.fail! "Error when requesting available fabric installer versions.\n#{e.inspect}"
117
108
  end
@@ -1,10 +1,14 @@
1
1
  module Mmi
2
2
  module Modloader
3
3
  class None
4
- def initialize(options=nil)
4
+ def initialize
5
5
  # Not installing anything requires no configuration or setup.
6
6
  end
7
7
 
8
+ def self.parse(*)
9
+ new
10
+ end
11
+
8
12
  def install
9
13
  # Nothing to do.
10
14
  end
@@ -0,0 +1,192 @@
1
+ module Mmi
2
+ module PropertyAttributes
3
+ def initialize(hash)
4
+ @hash = hash
5
+ end
6
+
7
+ def to_h
8
+ @hash
9
+ end
10
+
11
+ def [](key)
12
+ @hash[key]
13
+ end
14
+
15
+ module ClassMethods
16
+ PropertyConfiguration = Struct.new(:key, :type, :required, :default, :requires, :conflicts, :validate)
17
+
18
+ def registered_properties
19
+ @registered_properties ||= {}
20
+ end
21
+
22
+ def property(method_name, key=method_name.to_s, type: nil, required: nil, default: nil, requires: [], conflicts: [], validate: nil)
23
+ raise(ArgumentError, '"method_name" must be a Symbol' ) if !method_name.is_a?(Symbol)
24
+ raise(ArgumentError, %Q{#{type.inspect} is not a valid "type"}) if !valid_atomic_type?(type) && !valid_hash_type?(type)
25
+
26
+ requires = [requires ] if !requires .is_a?(Array)
27
+ conflicts = [conflicts] if !conflicts.is_a?(Array)
28
+
29
+ raise(ArgumentError, '"required" is mutually exclusive with "requires" and "conflicts"') if required && (requires.any? || conflicts.any?)
30
+ raise(ArgumentError, '"requires" and "conflicts" must not share entries' ) if requires.intersect?(conflicts)
31
+ raise(ArgumentError, '"required" is mutually exclusive with "default"' ) if required && !default.nil?
32
+
33
+ required = requires.none? && conflicts.none? && default.nil? if required.nil?
34
+
35
+ raise(ArgumentError, '"validate" must be nil, a symbol or a Proc') if !validate.nil? && !validate.is_a?(Symbol) && !validate.is_a?(Proc)
36
+
37
+ registered_properties[method_name] = PropertyConfiguration.new(key: key, type: type, required: required, default: default, requires: requires, conflicts: conflicts, validate: validate)
38
+
39
+ define_method(method_name) do
40
+ parsed_property_store[method_name]
41
+ end
42
+ end
43
+
44
+ def parse(hash)
45
+ new(hash).parse!
46
+ end
47
+
48
+ def valid_atomic_type?(type)
49
+ type.nil? || type.respond_to?(:parse)
50
+ end
51
+
52
+ def valid_hash_type?(type_hash)
53
+ type_hash.is_a?(Hash) && type_hash.key?(:field) && type_hash.key?(:types) && type_hash.fetch(:types).is_a?(Hash) && type_hash.fetch(:types).all? do |_, type|
54
+ valid_atomic_type?(type)
55
+ end
56
+ end
57
+ end
58
+
59
+ def self.prepended(klass)
60
+ klass.extend(ClassMethods)
61
+ end
62
+
63
+ def parsed_property_store
64
+ @parsed_property_store ||= {}
65
+ end
66
+
67
+ class ValidationError < StandardError; end
68
+
69
+ def validate_constraints!
70
+ errors =
71
+ self.class.registered_properties.map do |method_name, configuration|
72
+ [
73
+ method_name,
74
+ [].tap do |property_errors|
75
+ if configuration.required && !@hash.key?(configuration.key)
76
+ property_errors << "missing field #{configuration.key}"
77
+ elsif !configuration.required && @hash.key?(configuration.key)
78
+ if (missing_requires = configuration.requires.reject{ |e| @hash.key?(e) }).any?
79
+ property_errors << "missing required field(s) #{missing_requires.map(&:inspect).join(', ')}"
80
+ end
81
+
82
+ if (present_conflicts = configuration.conflicts.select{ |e| @hash.key?(e) }).any?
83
+ property_errors << "conflicting with field(s) #{present_conflicts.map(&:inspect).join(', ')}"
84
+ end
85
+ end
86
+
87
+ if @hash.key?(configuration.key) && self.class.valid_hash_type?(configuration.type)
88
+ if !@hash.fetch(configuration.key).is_a?(Hash)
89
+ property_errors << "field #{configuration.key.inspect} must be a Hash"
90
+ elsif !@hash.fetch(configuration.key).key?(configuration.type[:field]) || !configuration.type[:types].keys.include?(@hash.fetch(configuration.key).fetch(configuration.type[:field]))
91
+ property_errors << "field #{configuration.key.inspect} must have key #{configuration.type[:field].inspect} with one of values #{configuration.type[:types].keys.map(&:inspect).join(', ')}"
92
+ end
93
+ end
94
+
95
+ if @hash.key?(configuration.key) && configuration.validate
96
+ deduced_proc =
97
+ if configuration.validate.is_a?(Symbol)
98
+ self.class.method(configuration.validate)
99
+ else
100
+ configuration.validate
101
+ end
102
+
103
+ deduced_proc.call(@hash[configuration.key], property_errors)
104
+ end
105
+ end,
106
+ ]
107
+ end.select do |_, property_errors|
108
+ property_errors.any?
109
+ end.to_h
110
+
111
+ if errors.none?
112
+ true
113
+ else
114
+ raise(ValidationError, errors.map do |method_name, property_errors|
115
+ "#{method_name}: #{property_errors.join(', ')}"
116
+ end.join('; '))
117
+ end
118
+ end
119
+
120
+ def parse!
121
+ validate_constraints!
122
+
123
+ self.class.registered_properties.each do |method_name, configuration|
124
+ if !@hash.key?(configuration.key)
125
+ if configuration.required || !configuration.default.nil?
126
+ parsed_property_store[method_name] = configuration.default
127
+ else
128
+ # Do nothing.
129
+ end
130
+ elsif configuration.type.nil?
131
+ parsed_property_store[method_name] = self[configuration.key]
132
+ else
133
+ deduced_type =
134
+ if self.class.valid_hash_type?(configuration.type)
135
+ configuration.type[:types].fetch(self[configuration.key][configuration.type[:field]])
136
+ else
137
+ configuration.type
138
+ end
139
+
140
+ initializer_proc = proc do |item|
141
+ deduced_type.parse(item)
142
+ end
143
+
144
+ parsed_property_store[method_name] =
145
+ self[configuration.key].then do |value|
146
+ value.is_a?(Array) ? value.map(&initializer_proc) : initializer_proc[value]
147
+ end
148
+ end
149
+ end
150
+
151
+ self
152
+ end
153
+
154
+ def update_properties!(properties)
155
+ raise(ArgumentError, 'argument must be a Hash' ) if !properties.is_a?(Hash)
156
+ raise(ArgumentError, 'argument can only have keys that are defined properties') if (properties.keys - self.class.registered_properties.keys).any?
157
+
158
+ old_property_store = @parsed_property_store
159
+ @parsed_property_store = nil
160
+
161
+ old_properties = properties.map do |method_name, value|
162
+ key = self.class.registered_properties[method_name].key
163
+
164
+ old_value = @hash[key]
165
+
166
+ if !value.nil?
167
+ @hash[key] = value
168
+ elsif @hash.key?(key)
169
+ @hash.delete(key)
170
+ end
171
+
172
+ [method_name, old_value]
173
+ end
174
+
175
+ parse!
176
+ rescue ValidationError
177
+ @parsed_property_store = old_property_store
178
+
179
+ old_properties.each do |method_name, old_value|
180
+ key = self.class.registered_properties[method_name].key
181
+
182
+ if !old_value.nil?
183
+ @hash[key] = old_value
184
+ elsif @hash.key?(key)
185
+ @hash.delete(key)
186
+ end
187
+ end
188
+
189
+ raise
190
+ end
191
+ end
192
+ end
@@ -2,53 +2,23 @@ require 'fileutils'
2
2
  require 'open-uri'
3
3
 
4
4
  require 'mmi/github_api'
5
- require 'mmi/option_attributes'
5
+ require 'mmi/property_attributes'
6
6
 
7
7
  module Mmi
8
8
  module Source
9
9
  class Github
10
- include Mmi::OptionAttributes
10
+ prepend Mmi::PropertyAttributes
11
11
 
12
- opt_accessor :owner
13
- opt_accessor :repo
14
- opt_accessor :install_dir
15
- opt_accessor :filename
12
+ property :owner
13
+ property :repo
14
+ property :install_dir
15
+ property :filename, required: false
16
16
 
17
- opt_accessor :asset_id
18
- opt_accessor :release
19
- opt_accessor :file
17
+ property :asset_id, conflicts: %w[release file]
18
+ property :release, conflicts: 'asset_id', requires: 'file'
19
+ property :file, conflicts: 'asset_id', requires: 'release'
20
20
 
21
- def initialize(options)
22
- @options = options
23
-
24
- parse!
25
- end
26
-
27
- def parse!
28
- if self.owner
29
- if self.repo
30
- if self.install_dir
31
- if self.asset_id
32
- # Pass.
33
- elsif self.release
34
- if self.file
35
- # Pass.
36
- else
37
- raise Mmi::MissingAttributeError, 'Missing "source.file" from asset because "source.asset_id" is not provided.'
38
- end
39
- else
40
- raise Mmi::MissingAttributeError, 'Missing "source.release" from asset because "source.asset_id" is not provided.'
41
- end
42
- else
43
- raise Mmi::MissingAttributeError, 'Missing "source.install_dir" from asset.'
44
- end
45
- else
46
- raise Mmi::MissingAttributeError, 'Missing "source.repo" from asset.'
47
- end
48
- else
49
- raise Mmi::MissingAttributeError, 'Missing "source.owner" from asset.'
50
- end
51
- end
21
+ # TODO: Ensure that either :asset_id or [:release, :file] is given.
52
22
 
53
23
  def repository_url
54
24
  "https://github.com/#{self.owner}/#{self.repo}"
@@ -66,21 +36,10 @@ module Mmi
66
36
  end
67
37
  end
68
38
 
69
- def install(dir)
70
- install_dir = File.expand_path(self.install_dir, dir)
71
- filepath = File.join(install_dir, self.filename || (self.asset_id ? cached_asset_response.name : self.file))
72
-
73
- Mmi.info "Downloading #{download_url.inspect} into #{filepath.inspect}."
39
+ def install(install_record)
40
+ filepath = File.join(install_dir, self.filename || (self.asset_id ? cached_asset_response.name : self.file))
74
41
 
75
- FileUtils.mkdir_p(install_dir)
76
-
77
- begin
78
- stream = URI.parse(download_url).open
79
-
80
- IO.copy_stream(stream, filepath)
81
- rescue OpenURI::HTTPError => e
82
- Mmi.fail! "Error when requesting asset.\n#{e.inspect}"
83
- end
42
+ install_record.add(download_url, filepath, content_hash: nil) # As of 2024-07-18, the GitHub API does not return any hash over the asset.
84
43
  end
85
44
 
86
45
  def display_name
@@ -2,75 +2,42 @@ require 'fileutils'
2
2
  require 'open-uri'
3
3
 
4
4
  require 'mmi/modrinth_api'
5
- require 'mmi/option_attributes'
5
+ require 'mmi/property_attributes'
6
+ require 'mmi/content_hash/sha512'
6
7
 
7
8
  module Mmi
8
9
  module Source
9
10
  class Modrinth
10
- include Mmi::OptionAttributes
11
+ prepend Mmi::PropertyAttributes
11
12
 
12
- opt_accessor :name
13
- opt_accessor :version
14
- opt_accessor :version_file
15
-
16
- opt_accessor :install_dir
17
- opt_accessor :filename
18
-
19
- def initialize(options)
20
- @options = options
21
-
22
- parse!
23
- end
24
-
25
- def parse!
26
- if self.name
27
- if self.version
28
- if self.version_file
29
- if self.install_dir
30
- # Pass.
31
- else
32
- raise Mmi::MissingAttributeError, 'Missing "source.install_dir" from asset.'
33
- end
34
- else
35
- raise Mmi::MissingAttributeError, 'Missing "source.version_file" from asset.'
36
- end
37
- else
38
- raise Mmi::MissingAttributeError, 'Missing "source.version" from asset.'
39
- end
40
- else
41
- raise Mmi::MissingAttributeError, 'Missing "source.name" from asset.'
42
- end
43
- end
13
+ property :name
14
+ property :version
15
+ property :version_file
16
+ property :install_dir
17
+ property :filename, required: false
44
18
 
45
19
  def cached_mod_versions(loader: nil, game_version: nil)
46
- (@cached_mod_versions ||= Hash.new)[{loader: loader, game_version: game_version}] ||= Mmi::ModrinthApi.project_versions(self.name, loader: loader, game_version: game_version)
20
+ (@cached_mod_versions ||= {})[{loader: loader, game_version: game_version}] ||= Mmi::ModrinthApi.project_versions(self.name, loader: loader, game_version: game_version)
47
21
  end
48
22
 
49
- def download_url
23
+ def api_version_file
50
24
  cached_mod_versions.select do |version|
51
25
  version['name'] == self.version
52
26
  end.map do |version|
53
27
  version['files']
54
28
  end.flatten(1).select do |files|
55
29
  files['filename'] == self.version_file
56
- end.first['url'].gsub(/ /, '%20')
30
+ end.first
57
31
  end
58
32
 
59
- def install(dir)
60
- install_dir = File.expand_path(self.install_dir, dir)
61
- filepath = File.join(install_dir, self.filename || self.version_file)
62
-
63
- Mmi.info "Downloading #{download_url.inspect} into #{filepath.inspect}."
64
-
65
- FileUtils.mkdir_p(install_dir)
33
+ def download_url
34
+ api_version_file['url'].gsub(/ /, '%20')
35
+ end
36
+
37
+ def install(install_record)
38
+ filepath = File.join(install_dir, self.filename || self.version_file)
66
39
 
67
- begin
68
- stream = URI.parse(download_url).open
69
-
70
- IO.copy_stream(stream, filepath)
71
- rescue OpenURI::HTTPError => e
72
- Mmi.fail! "Error when requesting asset.\n#{e.inspect}"
73
- end
40
+ install_record.add(download_url, filepath, content_hash: Mmi::ContentHash::Sha512.new(api_version_file['hashes']['sha512']))
74
41
  end
75
42
 
76
43
  def display_name
@@ -1,54 +1,25 @@
1
1
  require 'fileutils'
2
2
  require 'open-uri'
3
3
 
4
- require 'mmi/option_attributes'
4
+ require 'mmi/property_attributes'
5
5
 
6
6
  module Mmi
7
7
  module Source
8
8
  class Url
9
- include Mmi::OptionAttributes
9
+ prepend Mmi::PropertyAttributes
10
10
 
11
- opt_accessor :url
12
- opt_accessor :install_dir
13
- opt_accessor :filename
14
-
15
- def initialize(options)
16
- @options = options
17
-
18
- parse!
19
- end
20
-
21
- def parse!
22
- if self.url
23
- if self.install_dir
24
- # Pass.
25
- else
26
- raise Mmi::MissingAttributeError, 'Missing "source.install_dir" from asset.'
27
- end
28
- else
29
- raise Mmi::MissingAttributeError, 'Missing "source.name" from asset.'
30
- end
31
- end
11
+ property :url
12
+ property :install_dir
13
+ property :filename, required: false
32
14
 
33
15
  def download_uri
34
16
  @download_uri ||= URI.parse(url)
35
17
  end
36
18
 
37
- def install(dir)
38
- install_dir = File.expand_path(self.install_dir, dir)
39
- filepath = File.join(install_dir, self.filename || File.basename(download_uri.path))
40
-
41
- Mmi.info "Downloading #{url.inspect} into #{filepath.inspect}."
42
-
43
- FileUtils.mkdir_p(install_dir)
19
+ def install(install_record)
20
+ filepath = File.join(install_dir, self.filename || File.basename(download_uri.path))
44
21
 
45
- begin
46
- stream = download_uri.open
47
-
48
- IO.copy_stream(stream, filepath)
49
- rescue OpenURI::HTTPError => e
50
- Mmi.fail! "Error when requesting asset.\n#{e.inspect}"
51
- end
22
+ install_record.add(url, filepath, content_hash: nil)
52
23
  end
53
24
 
54
25
  def display_name
data/lib/mmi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mmi
2
- VERSION = '0.2.2'.freeze
2
+ VERSION = '0.2.3'.freeze
3
3
  end
data/lib/mmi.rb CHANGED
@@ -2,17 +2,6 @@ require 'mmi/version'
2
2
  require 'mmi/mod_file_processor'
3
3
 
4
4
  module Mmi
5
- MMI_CACHE_DIR = File.join(Dir.home, '.cache', 'mmi')
6
- MINECRAFT_DIR = File.join(Dir.home, '.minecraft')
7
-
8
- def self.cache_dir
9
- MMI_CACHE_DIR
10
- end
11
-
12
- def self.minecraft_dir
13
- MINECRAFT_DIR
14
- end
15
-
16
5
  class ValidationError < StandardError; end
17
6
  class MissingAttributeError < ValidationError; end
18
7
  class InvalidAttributeError < ValidationError; end