smc-get 0.2.0.beta1 → 0.3.0.beta1

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.
@@ -1,80 +1,186 @@
1
- #Encoding: UTF-8
2
- ################################################################################
3
- # This file is part of smc-get.
4
- # Copyright (C) 2010-2011 Entertaining Software, Inc.
5
- # Copyright (C) 2011 Marvin Gülker
6
- #
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU General Public License as published by
9
- # the Free Software Foundation, either version 3 of the License, or
10
- # (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
- ################################################################################
20
-
21
- module SmcGet
22
-
23
- #This class allows for easy compression and decompression of .smcpak
24
- #files. Please note that during this process no validations are performed,
25
- #i.e. you have to ensure that the directory you want to compress is in
26
- #smc package layout and the .smcpak files you want to decompress are
27
- #really smc packages.
28
- class PackageArchive
29
-
30
- #The location of this archive.
31
- attr_reader :path
32
-
33
- #Compresses all files in +directory+ into a TAR.XZ file with the
34
- #extension ".smcpak".
35
- #Returns an object of this class.
36
- # smcpak = PackageArchive.compress("mypackage")
37
- # puts smcpak.path #=> mypackage.smcpak
38
- def self.compress(directory, goal_file)
39
- directory = Pathname.new(directory).expand_path
40
- tar_file = Pathname.new("#{goal_file}.tar").expand_path
41
- xz_file = Pathname.new(goal_file).expand_path
42
-
43
- Dir.chdir(directory.parent) do
44
- tar_file.open("wb") do |file|
45
- Archive::Tar::Minitar.pack(directory.relative_path_from(Pathname.new(".").expand_path).to_s, file)
46
- end
47
- end
48
- XZ.compress_file(tar_file, xz_file)
49
-
50
- tar_file.delete #We don’t need it anymore
51
-
52
- new(xz_file)
53
- end
54
-
55
- #Creates a new PackageArchive from an existing .smcpak file.
56
- def initialize(archive)
57
- @path = Pathname.new(archive)
58
- end
59
-
60
- #Decompresses this archive into +directory+, creating a subdirectory
61
- #named after the archive without the extension, and returns the path
62
- #to that subdirectory (a Pathname object).
63
- # smcpak = PackageArchive.new("mypackage.smcpak")
64
- # puts smcpak.decompress #=> #<Pathname:mypackage>
65
- def decompress(directory)
66
- directory = Pathname.new(directory)
67
- tar_file = directory + @path.basename.to_s.sub(/\.smcpak$/, ".tar")
68
- dest_dir = directory + tar_file.basename.to_s.sub(/\.tar$/, "")
69
-
70
- XZ.decompress_file(@path.to_s, tar_file.to_s)
71
- Archive::Tar::Minitar.unpack(tar_file.to_s, dest_dir.to_s)
72
-
73
- tar_file.delete #We don’ŧ need it anymore
74
-
75
- dest_dir
76
- end
77
-
78
- end
79
-
80
- end
1
+ # -*- coding: utf-8 -*-
2
+ ################################################################################
3
+ # This file is part of smc-get.
4
+ # Copyright (C) 2010-2011 Entertaining Software, Inc.
5
+ # Copyright (C) 2011 Marvin Gülker
6
+ #
7
+ # This program is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ ################################################################################
20
+
21
+ module SmcGet
22
+
23
+ #This class allows for easy compression and decompression of .smcpak
24
+ #files. Please note that during this process no validations are performed,
25
+ #i.e. you have to ensure that the directory you want to compress is in
26
+ #smc package layout and the .smcpak files you want to decompress are
27
+ #really smc packages.
28
+ class PackageArchive
29
+
30
+ #Number of bytes to read from a file at a time when it’s being
31
+ #compressed.
32
+ CHUNK_SIZE = 4096
33
+
34
+ #The location of this archive. A Pathname object.
35
+ attr_reader :path
36
+
37
+ class << self
38
+
39
+ #Compresses all files in +directory+ into a TAR.XZ file with the
40
+ #extension ".smcpak".
41
+ #==Parameters
42
+ #[directory] The directory to compress. This will be the toplevel
43
+ # directory in the resulting TAR file.
44
+ #[goal_file] Full path to where to place the TAR file.
45
+ #==Return value
46
+ #Returns an object of this class.
47
+ #==Examples
48
+ # smcpak = PackageArchive.compress("/home/freak/packages/mypackage", "mypackage.smcpak")
49
+ # puts smcpak.path #=> mypackage.smcpak
50
+ #
51
+ # smcpak = PackageArchive.compress("mypackage", "/home/freak/compressed_packages/mypackage.smcpak")
52
+ # puts smcpak.path #=> /home/freak/compressed_packages/mypackage.smcpak
53
+ def compress(directory, goal_file)
54
+ directory = Pathname.new(directory).expand_path
55
+ tar_file = Pathname.new("#{goal_file}.tar").expand_path
56
+ xz_file = Pathname.new(goal_file).expand_path
57
+
58
+ tar_file.open("wb"){|file| compress_dir(directory, file)}
59
+ XZ.compress_file(tar_file, xz_file)
60
+
61
+ tar_file.delete #We don’t need it anymore
62
+
63
+ new(xz_file)
64
+ end
65
+
66
+ private
67
+
68
+ #This method creates a TAR archive inside the given +io+ by
69
+ #recursively packaging all files found in +path+ into it.
70
+ #
71
+ #Note that this method isn’t quite the same as <tt>Minitar.pack</tt>,
72
+ #because there is a big difference in how absolute paths are handled.
73
+ #Consider the following call:
74
+ #
75
+ # Minitar.pack("/home/freak/foo", file)
76
+ #
77
+ #Inside the TAR you’ll get this:
78
+ #
79
+ # /
80
+ # home/
81
+ # freak/
82
+ # foo/
83
+ # 1.txt
84
+ # 2.rb
85
+ #
86
+ #Surely not what you wanted, plus a leading / that can be misinterpreted
87
+ #by some archiving programs (e.g. XFCE’s XArchiver). Now compare that
88
+ #to what this method does:
89
+ #
90
+ # compress_dir("/home/freak/foo", file)
91
+ #
92
+ #Result in the TAR:
93
+ #
94
+ # foo/
95
+ # 1.txt
96
+ # 2.txt
97
+ #
98
+ #It strippes off the unwanted prefix and just places the actual contents of
99
+ #the last element in +path+ (and it’s subdirectories) inside the TAR
100
+ #archive. For a +path+ relative to the current working directory however
101
+ #this method should behave the same as the usual <tt>Minitar.pack</tt>.
102
+ #
103
+ #Extra bonus: This method is threadsafe, as it doesn’t use +chdir+ to
104
+ #change the directory representation in the TAR file. It uses Minitar’s
105
+ #lowlevel methods instead.
106
+ def compress_dir(path, io)
107
+ raise(ArgumentError, "#{path} is not a directory!") unless path.directory?
108
+
109
+ Archive::Tar::Minitar::Output.open(io) do |output|
110
+ tar = output.tar #Get the real Writer object
111
+
112
+ path.find do |entry|
113
+ #This is the path as it will show up inside the tar
114
+ relative_path = entry.relative_path_from(path.parent) #parent b/c first entry must be the toplevel dirname, not "."
115
+
116
+ #Copy permissions from the original file for the tar’ed file
117
+ stat = entry.stat
118
+ stats = {
119
+ :mode => stat.mode,
120
+ :mtime => stat.mtime,
121
+ :size => stat.size,
122
+ :uid => stat.uid, #Should be nil on Windows
123
+ :gid => stat.gid #Should be nil on Windows
124
+ }
125
+
126
+ if entry.directory?
127
+ tar.mkdir(relative_path.to_s, stats)
128
+ elsif entry.file?
129
+ tar.add_file_simple(relative_path.to_s, stats) do |stream|
130
+ File.open(entry, "rb") do |file|
131
+ chunk = nil
132
+ stream.write(chunk) while chunk = file.read(CHUNK_SIZE)
133
+ end
134
+ end
135
+ else
136
+ raise(Errors::CompressionError.new("Unsupported file type for #{entry}!", entry))
137
+ end #if
138
+ end #find
139
+ end #Output.new
140
+ end #compress_dir
141
+
142
+ end
143
+
144
+ #Creates a new PackageArchive from an existing .smcpak file.
145
+ #==Parameter
146
+ #[archive] The path to the file.
147
+ #==Return value
148
+ #A new instance of this class.
149
+ #==Example
150
+ # archive = PackageArchive.new("/home/freak/compressed_packages/mypackage.smcpak")
151
+ def initialize(archive)
152
+ @path = Pathname.new(archive)
153
+ end
154
+
155
+ #Decompresses this archive, creating a subdirectory
156
+ #named after the archive without the extension.
157
+ #==Parameters
158
+ #[directory] Where to extract the archive to. Note a subdirectory
159
+ # will be created below this path.
160
+ #==Return value
161
+ #The path to the subdirectory, a Pathname object.
162
+ #==Example
163
+ # smcpak = PackageArchive.new("mypackage.smcpak")
164
+ # puts smcpak.decompress #=> #<Pathname:mypackage>
165
+ def decompress(directory)
166
+ directory = Pathname.new(directory)
167
+ tar_file = directory + @path.basename.to_s.sub(/\.smcpak$/, ".tar")
168
+ dest_dir = directory + tar_file.basename.to_s.sub(/\.tar$/, "")
169
+
170
+ XZ.decompress_file(@path.to_s, tar_file.to_s)
171
+ Archive::Tar::Minitar.unpack(tar_file.to_s, dest_dir.to_s)
172
+
173
+ tar_file.delete #We don’t need it anymore
174
+
175
+ dest_dir
176
+ end
177
+
178
+ #Human-readable description of form:
179
+ # #<SmcGet::PackageArchive <path>>
180
+ def inspect
181
+ "#<#{self.class} #{@path.expand_path}>"
182
+ end
183
+
184
+ end
185
+
186
+ end
@@ -1,253 +1,257 @@
1
- #Encoding: UTF-8
2
- ################################################################################
3
- # This file is part of smc-get.
4
- # Copyright (C) 2010-2011 Entertaining Software, Inc.
5
- # Copyright (C) 2011 Marvin Gülker
6
- #
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU General Public License as published by
9
- # the Free Software Foundation, either version 3 of the License, or
10
- # (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
- ################################################################################
20
-
21
- module SmcGet
22
-
23
- #A PackageSpecification object is a mostly informational object
24
- #directly related to Package objects. That is, each and every
25
- #Package instance has a Package#spec method that will retrieve
26
- #the PackageSpecification, an instance of this class, for you:
27
- #
28
- # puts pkg.spec.title #=> Cool package
29
- #
30
- #Instances of this class can be understood as the parsed content of
31
- #a package’s specification file. It has getter and setter methods
32
- #reflecting the keys that are allowed in the specification (see
33
- #the smcpak.rdoc file), but unless you want to build smc packages,
34
- #there’s no need for you to use the setter methods.
35
- class PackageSpecification
36
-
37
- #The keys listed here must be mentioned inside a package spec,
38
- #otherwise the package is considered broken.
39
- SPEC_MANDATORY_KEYS = [:title, :last_update, :authors, :difficulty, :description, :checksums].freeze
40
-
41
- ##
42
- # :attr_accessor: title
43
- #The package’s title.
44
-
45
- ##
46
- # :attr_accessor: last_update
47
- #The time (an instance of class Time) indicating when the package
48
- #was last updated.
49
-
50
- ##
51
- # :attr_accessor: authors
52
- #The authors of this package. An array.
53
-
54
- ##
55
- # :attr_accessor: difficulty
56
- #The difficulty of this package as a string.
57
-
58
- ##
59
- # :attr_accessor: description
60
- #The description of this package.
61
-
62
- ##
63
- # :attr_accessor: install_message
64
- #A message to display during installation of this package or nil if
65
- #no message shall be displayed.
66
-
67
- ##
68
- # :attr_accessor: remove_message
69
- #A message to display during removing of this package or nil if no
70
- #message shall be displayed.
71
-
72
- ##
73
- # :attr_accessor: dependecies
74
- #An array of package names this package depends on, i.e. packages that
75
- #need to be installed before this one can be installed. The array is
76
- #empty if no dependecies exist.
77
-
78
- ##
79
- # :attr_accessor: levels
80
- #An array of level file names (strings).
81
-
82
- ##
83
- # :attr_accessor: music
84
- #An array of music file names (strings).
85
-
86
- ##
87
- # :attr_accessor: sounds
88
- #An array of sound file names (strings).
89
-
90
- ##
91
- # :attr_accessor: graphics
92
- #An array of graphic file names (strings).
93
-
94
- ##
95
- # :attr_accessor: worlds
96
- #An array of graphic file names (strings).
97
-
98
- ##
99
- # :attr_accessor: checksums
100
- #A hash that maps each filename in this package to it’s SHA1 checksum.
101
-
102
- #The name of the package this specification is used in, without any
103
- #file extension.
104
- attr_reader :name
105
-
106
- ##
107
- # :attr_reader: compressed_file_name
108
- #The name of the compressed file this specification should belong to.
109
- #The same as name, but the extension .smcpak was appended.
110
-
111
- ##
112
- # :attr_reader: spec_file_name
113
- #The name of the specification file. The same as name, but
114
- #the extension .yml was appended.
115
-
116
- #Creates a PackageSpecification by directly reading a complete
117
- #spec from a YAML file.
118
- #==Parameter
119
- #[path] The path to the file to read.
120
- #==Return value
121
- #An instance of this class.
122
- #==Raises
123
- #[InvalidSpecification] +path+ was not found or was malformatted.
124
- #==Example
125
- # path = remote_repo.fetch_spec("cool_pkg.yml")
126
- # spec = SmcGet::PackageSpecification.from_file(path)
127
- # puts spec.title #=> "Cool package"
128
- #==Remarks
129
- #This method may be useful if you don’t need a full-blown
130
- #Package object and just want to deal with it’s most important
131
- #attributes.
132
- def self.from_file(path)
133
- info = nil
134
- begin
135
- info = YAML.load_file(path.to_s)
136
- rescue Errno::ENOENT => e
137
- raise(Errors::InvalidSpecification, "File '#{path}' doesn't exist!")
138
- rescue => e
139
- raise(Errors::InvalidSpecification, "Invalid YAML: #{e.message}")
140
- end
141
-
142
- spec = new(File.basename(path).sub(/\.yml$/, ""))
143
- info.each_pair do |key, value|
144
- spec.send(:"#{key}=", value)
145
- end
146
- #TODO: Convert the strings in :checksums to strings, except the
147
- #filenames, those should be strings. Anyone???
148
-
149
- raise(Errors::InvalidSpecification, spec.validate.first) unless spec.valid?
150
-
151
- spec
152
- end
153
-
154
- #Returns the matching package name from the package specification’s name
155
- #by replacing the .yml extension with .smcpak.
156
- #==Return value
157
- #A string ending in ".smcpak".
158
- #==Example
159
- # p SmcGet::PackageSpecification.spec2pkg("cool_pkg.yml") #=> "cool_pkg.smcpak")
160
- def self.spec2pkg(spec_file_name) # :nodoc:
161
- spec_file_name.to_s.sub(/\.yml$/, ".smcpak")
162
- end
163
-
164
- #Returns the matching specification file name from the package’s name
165
- #by replacing the .smcpak extension with .yml.
166
- #==Return value
167
- #A string ending in ".yml"
168
- #==Example
169
- # p SmcGet::PackageSpecification.pkg2spec("cool_pkg.smcpak") #=> "cool_pkg.yml"
170
- def self.pkg2spec(package_file_name) # :nodoc:
171
- package_file_name.to_s.sub(/\.smcpak$/, ".yml")
172
- end
173
-
174
- def initialize(pkg_name)
175
- @info = {:dependencies => [], :levels => [], :music => [], :sounds => [], :graphics => [], :worlds => []}
176
- @name = pkg_name
177
- end
178
-
179
- #See attribute.
180
- def compressed_file_name # :nodoc:
181
- "#@name.smcpak"
182
- end
183
-
184
- #See attribute.
185
- def spec_file_name # :nodoc:
186
- "#@name.yml"
187
- end
188
-
189
- [:title, :last_update, :authors, :difficulty, :description, :install_message, :remove_message, :dependencies, :levels, :music, :sounds, :graphics, :worlds, :checksums].each do |sym|
190
- define_method(sym){@info[sym]}
191
- define_method(:"#{sym}="){|val| @info[sym] = val}
192
- end
193
-
194
- def [](sym)
195
- if respond_to?(sym)
196
- send(sym)
197
- else
198
- raise(IndexError, "No such specification key: #{sym}!")
199
- end
200
- end
201
-
202
- def valid?
203
- validate.empty?
204
- end
205
-
206
- def validate
207
- errors = []
208
-
209
- SPEC_MANDATORY_KEYS.each do |sym|
210
- errors << "Mandatory key #{sym} is missing!" unless @info.has_key?(sym)
211
- end
212
-
213
- errors
214
- end
215
-
216
- #Compares two specifications. They are considered equal if all their
217
- #attributes (levels, difficulty, etc.) are equal.
218
- def ==(other)
219
- return false unless other.respond_to? :info
220
- @info == other.info
221
- end
222
-
223
- #Saves the package specification in YAML format into a file.
224
- #==Parameter
225
- #[directory] The directory where to save the file to. The filename is automatically
226
- # detected from the attributes set for the specification.
227
- #==Raises
228
- #[InvalidSpecification] The specification was not valid, i.e. contained incorrect
229
- # or missing values.
230
- #==Example
231
- # p spec.name #=> "cool_pkg"
232
- # spec.save(".")
233
- # p File.file?("cool_pkg.yml") #=> true
234
- def save(directory)
235
- raise(Errors::InvalidSpecification, validate.first) unless valid?
236
-
237
- path = Pathname.new(directory) + "#{@name}.yml"
238
- #Turn the spec keys for serialization into strings
239
- hsh = {}
240
- @info.each_pair{|k, v| hsh[k.to_s] = v}
241
- path.open("w"){|f| YAML.dump(hsh, f)}
242
- end
243
-
244
- protected
245
-
246
- #Returns the complete internal information hash.
247
- def info
248
- @info
249
- end
250
-
251
- end
252
-
253
- end
1
+ #Encoding: UTF-8
2
+ ################################################################################
3
+ # This file is part of smc-get.
4
+ # Copyright (C) 2010-2011 Entertaining Software, Inc.
5
+ # Copyright (C) 2011 Marvin Gülker
6
+ #
7
+ # This program is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ ################################################################################
20
+
21
+ module SmcGet
22
+
23
+ #A PackageSpecification object is a mostly informational object
24
+ #directly related to Package objects. That is, each and every
25
+ #Package instance has a Package#spec method that will retrieve
26
+ #the PackageSpecification, an instance of this class, for you:
27
+ #
28
+ # puts pkg.spec.title #=> Cool package
29
+ #
30
+ #Instances of this class can be understood as the parsed content of
31
+ #a package’s specification file. It has getter and setter methods
32
+ #reflecting the keys that are allowed in the specification (see
33
+ #the smcpak.rdoc file), but unless you want to build smc packages,
34
+ #there’s no need for you to use the setter methods.
35
+ class PackageSpecification
36
+
37
+ #The keys listed here must be mentioned inside a package spec,
38
+ #otherwise the package is considered broken.
39
+ SPEC_MANDATORY_KEYS = [:title, :last_update, :authors, :difficulty, :description, :checksums].freeze
40
+
41
+ ##
42
+ # :attr_accessor: title
43
+ #The package’s title.
44
+
45
+ ##
46
+ # :attr_accessor: last_update
47
+ #The time (an instance of class Time) indicating when the package
48
+ #was last updated.
49
+
50
+ ##
51
+ # :attr_accessor: authors
52
+ #The authors of this package. An array.
53
+
54
+ ##
55
+ # :attr_accessor: difficulty
56
+ #The difficulty of this package as a string.
57
+
58
+ ##
59
+ # :attr_accessor: description
60
+ #The description of this package.
61
+
62
+ ##
63
+ # :attr_accessor: install_message
64
+ #A message to display during installation of this package or nil if
65
+ #no message shall be displayed.
66
+
67
+ ##
68
+ # :attr_accessor: remove_message
69
+ #A message to display during removing of this package or nil if no
70
+ #message shall be displayed.
71
+
72
+ ##
73
+ # :attr_accessor: dependecies
74
+ #An array of package names this package depends on, i.e. packages that
75
+ #need to be installed before this one can be installed. The array is
76
+ #empty if no dependecies exist.
77
+
78
+ ##
79
+ # :attr_accessor: levels
80
+ #An array of level file names (strings).
81
+
82
+ ##
83
+ # :attr_accessor: music
84
+ #An array of music file names (strings).
85
+
86
+ ##
87
+ # :attr_accessor: sounds
88
+ #An array of sound file names (strings).
89
+
90
+ ##
91
+ # :attr_accessor: graphics
92
+ #An array of graphic file names (strings).
93
+
94
+ ##
95
+ # :attr_accessor: worlds
96
+ #An array of graphic file names (strings).
97
+
98
+ ##
99
+ # :attr_accessor: checksums
100
+ #A hash that maps each filename in this package to it’s SHA1 checksum.
101
+
102
+ #The name of the package this specification is used in, without any
103
+ #file extension.
104
+ attr_reader :name
105
+
106
+ ##
107
+ # :attr_reader: compressed_file_name
108
+ #The name of the compressed file this specification should belong to.
109
+ #The same as name, but the extension .smcpak was appended.
110
+
111
+ ##
112
+ # :attr_reader: spec_file_name
113
+ #The name of the specification file. The same as name, but
114
+ #the extension .yml was appended.
115
+
116
+ #Creates a PackageSpecification by directly reading a complete
117
+ #spec from a YAML file.
118
+ #==Parameter
119
+ #[path] The path to the file to read.
120
+ #==Return value
121
+ #An instance of this class.
122
+ #==Raises
123
+ #[InvalidSpecification] +path+ was not found or was malformatted.
124
+ #==Example
125
+ # path = remote_repo.fetch_spec("cool_pkg.yml")
126
+ # spec = SmcGet::PackageSpecification.from_file(path)
127
+ # puts spec.title #=> "Cool package"
128
+ #==Remarks
129
+ #This method may be useful if you don’t need a full-blown
130
+ #Package object and just want to deal with it’s most important
131
+ #attributes.
132
+ def self.from_file(path)
133
+ info = nil
134
+ begin
135
+ info = YAML.load_file(path.to_s)
136
+ rescue Errno::ENOENT => e
137
+ raise(Errors::InvalidSpecification, "File '#{path}' doesn't exist!")
138
+ rescue => e
139
+ raise(Errors::InvalidSpecification, "Invalid YAML: #{e.message}")
140
+ end
141
+
142
+ spec = new(File.basename(path).sub(/\.yml$/, ""))
143
+ info.each_pair do |key, value|
144
+ spec.send(:"#{key}=", value)
145
+ end
146
+ #TODO: Convert the strings in :checksums to strings, except the
147
+ #filenames, those should be strings. Anyone???
148
+
149
+ raise(Errors::InvalidSpecification, spec.validate.first) unless spec.valid?
150
+
151
+ spec
152
+ end
153
+
154
+ #Returns the matching package name from the package specification’s name
155
+ #by replacing the .yml extension with .smcpak.
156
+ #==Return value
157
+ #A string ending in ".smcpak".
158
+ #==Example
159
+ # p SmcGet::PackageSpecification.spec2pkg("cool_pkg.yml") #=> "cool_pkg.smcpak")
160
+ def self.spec2pkg(spec_file_name) # :nodoc:
161
+ spec_file_name.to_s.sub(/\.yml$/, ".smcpak")
162
+ end
163
+
164
+ #Returns the matching specification file name from the package’s name
165
+ #by replacing the .smcpak extension with .yml.
166
+ #==Return value
167
+ #A string ending in ".yml"
168
+ #==Example
169
+ # p SmcGet::PackageSpecification.pkg2spec("cool_pkg.smcpak") #=> "cool_pkg.yml"
170
+ def self.pkg2spec(package_file_name) # :nodoc:
171
+ package_file_name.to_s.sub(/\.smcpak$/, ".yml")
172
+ end
173
+
174
+ def initialize(pkg_name)
175
+ @info = {:dependencies => [], :levels => [], :music => [], :sounds => [], :graphics => [], :worlds => []}
176
+ @name = pkg_name
177
+ end
178
+
179
+ #See attribute.
180
+ def compressed_file_name # :nodoc:
181
+ "#@name.smcpak"
182
+ end
183
+
184
+ #See attribute.
185
+ def spec_file_name # :nodoc:
186
+ "#@name.yml"
187
+ end
188
+
189
+ [:title, :last_update, :authors, :difficulty, :description, :install_message, :remove_message, :dependencies, :levels, :music, :sounds, :graphics, :worlds, :checksums].each do |sym|
190
+ define_method(sym){@info[sym]}
191
+ define_method(:"#{sym}="){|val| @info[sym] = val}
192
+ end
193
+
194
+ def [](sym)
195
+ if respond_to?(sym)
196
+ send(sym)
197
+ else
198
+ raise(IndexError, "No such specification key: #{sym}!")
199
+ end
200
+ end
201
+
202
+ def valid?
203
+ validate.empty?
204
+ end
205
+
206
+ def validate
207
+ errors = []
208
+
209
+ SPEC_MANDATORY_KEYS.each do |sym|
210
+ errors << "Mandatory key #{sym} is missing!" unless @info.has_key?(sym)
211
+ end
212
+
213
+ [:levels, :graphics, :music, :sounds, :worlds].each do |sym|
214
+ errors << "Mandatory checksum key #{sym} is missing!" unless @info[:checksums].has_key?(sym.to_s)
215
+ end
216
+
217
+ errors
218
+ end
219
+
220
+ #Compares two specifications. They are considered equal if all their
221
+ #attributes (levels, difficulty, etc.) are equal.
222
+ def ==(other)
223
+ return false unless other.respond_to? :info
224
+ @info == other.info
225
+ end
226
+
227
+ #Saves the package specification in YAML format into a file.
228
+ #==Parameter
229
+ #[directory] The directory where to save the file to. The filename is automatically
230
+ # detected from the attributes set for the specification.
231
+ #==Raises
232
+ #[InvalidSpecification] The specification was not valid, i.e. contained incorrect
233
+ # or missing values.
234
+ #==Example
235
+ # p spec.name #=> "cool_pkg"
236
+ # spec.save(".")
237
+ # p File.file?("cool_pkg.yml") #=> true
238
+ def save(directory)
239
+ raise(Errors::InvalidSpecification, validate.first) unless valid?
240
+
241
+ path = Pathname.new(directory) + "#{@name}.yml"
242
+ #Turn the spec keys for serialization into strings
243
+ hsh = {}
244
+ @info.each_pair{|k, v| hsh[k.to_s] = v}
245
+ path.open("w"){|f| YAML.dump(hsh, f)}
246
+ end
247
+
248
+ protected
249
+
250
+ #Returns the complete internal information hash.
251
+ def info
252
+ @info
253
+ end
254
+
255
+ end
256
+
257
+ end