smc-get 0.1.0 → 0.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/smc_get/gui.rb CHANGED
@@ -1,19 +1,20 @@
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
- ################################################################################
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
+ # vim:set ts=8 sts=2 sw=2 et: #
@@ -0,0 +1,277 @@
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 LocalRepository contains all the packages that are installed locally, i.e.
24
+ #that have been downloaded and added to your local SMC installation.
25
+ #It provides the exact same methods as RemoteRepository, but works completely
26
+ #local and therefore doesn’t need an internet connection as RemoteRepository does.
27
+ #
28
+ #The only notable difference is that instances of this class have some
29
+ #attributes different from those defined for RemoteRepository, but usually
30
+ #you shouldn’t have to worry about that.
31
+ #
32
+ #Concluding, you’ll find the documentation of most of the methods of this class
33
+ #in the documentation of the RemoteRepository class, because duplicating docs
34
+ #doesn’t make much sense.
35
+ class LocalRepository < Repository
36
+
37
+ #Directory where the package specs are kept.
38
+ SPECS_DIR = Pathname.new("packages")
39
+ #Directory where downloaded packages are cached.
40
+ CACHE_DIR = Pathname.new("cache")
41
+ #Directory where the packages’ level files are kept.
42
+ CONTRIB_LEVELS_DIR = Pathname.new("levels") #Levels in subdirectories are currently not recognized by SMC
43
+ #Directory where the packages’ music files are kept.
44
+ CONTRIB_MUSIC_DIR = Pathname.new("music") + "contrib-music"
45
+ #Directory where the packages’ graphic files are kept.
46
+ CONTRIB_GRAPHICS_DIR = Pathname.new("pixmaps") + "contrib-graphics"
47
+ #Directory where the packages’ sound files are kept.
48
+ CONTRIB_SOUNDS_DIR = Pathname.new("sounds") + "contrib-sounds"
49
+ #Directory where the packages’ world files are kept
50
+ CONTRIB_WORLDS_DIR = Pathname.new("world") #Worlds in subdirectores are currently not recognized by SMC
51
+
52
+ #Root path of the local repository. Should be the same as your SMC’s
53
+ #installation path.
54
+ attr_reader :path
55
+ #This repository’s specs dir.
56
+ attr_reader :specs_dir
57
+ #This repository’s cache dir.
58
+ attr_reader :cache_dir
59
+ #This repository’s package levels dir.
60
+ attr_reader :contrib_level_dir
61
+ #This repository’s package music dir.
62
+ attr_reader :contrib_music_dir
63
+ #This repository’s package graphics dir.
64
+ attr_reader :contrib_graphics_dir
65
+ #This repository’s package sounds dir.
66
+ attr_reader :contrib_sounds_dir
67
+ #This repository’s package worlds dir.
68
+ attr_reader :contrib_worlds_dir
69
+ #An array of PackageSpecification objects containing the specs of
70
+ #all packages installed in this repository.
71
+ attr_reader :package_specs
72
+
73
+ #"Creates" a new local repository whose root is located at the given +path+.
74
+ #When instanciating this class, you should point it to the root of your
75
+ #SMC installation’s *share* directory, e.g. <b>/usr/share/smc</b>.
76
+ #==Parameter
77
+ #[path] The path to your SMC installation.
78
+ #==Return value
79
+ #The newly created LocalRepository.
80
+ #==Example
81
+ # lr = SmcGet::LocalRepository.new("/usr/share/smc")
82
+ #==Remarks
83
+ #smc-get requires some additional directories in your SMC installation,
84
+ #namely (where +smc+ is your SMC’s *share* directory):
85
+ # * smc/packages
86
+ # * smc/music/contrib-music
87
+ # * smc/sounds/contrib-sounds
88
+ # * smc/pixmaps/contrib-graphics
89
+ #These will be created when you call this method, so make sure
90
+ #you have the appropriate permissions for these directories or
91
+ #you’ll get an Errno::EACCES exception when calling ::new.
92
+ def initialize(path)
93
+ @path = Pathname.new(path)
94
+ @specs_dir = @path + SPECS_DIR
95
+ @cache_dir = @path + CACHE_DIR
96
+ @levels_dir = @path + CONTRIB_LEVELS_DIR
97
+ @music_dir = @path + CONTRIB_MUSIC_DIR
98
+ @graphics_dir = @path + CONTRIB_GRAPHICS_DIR
99
+ @sounds_dir = @path + CONTRIB_SOUNDS_DIR
100
+ @worlds_dir = @path + CONTRIB_WORLDS_DIR
101
+
102
+ #Create the directories if they’re not there yet
103
+ [@specs_dir, @cache_dir, @levels_dir, @music_dir, @graphics_dir, @sounds_dir, @worlds_dir].each do |dir|
104
+ dir.mkpath unless dir.directory?
105
+ end
106
+
107
+ @package_specs = []
108
+ @specs_dir.children.each do |spec_path|
109
+ next unless spec_path.to_s.end_with?(".yml")
110
+ @package_specs << PackageSpecification.from_file(spec_path)
111
+ end
112
+ end
113
+
114
+ def fetch_spec(spec_file, directory = ".")
115
+ directory = Pathname.new(directory)
116
+
117
+ spec_file_path = @specs_dir + spec_file
118
+ raise(Errors::NoSuchResourceError.new(:spec, spec_file), "Package specification '#{spec_file}' not found in the local repository '#{to_s}'!") unless spec_file_path.file?
119
+
120
+ directory.mktree unless directory.directory?
121
+
122
+ #No need to really "fetch" the spec--this is a *local* repository.
123
+ FileUtils.cp(spec_file_path, directory)
124
+ directory + spec_file
125
+ end
126
+
127
+ def fetch_package(pkg_file, directory = ".")
128
+ directory = Pathname.new(directory)
129
+
130
+ pkg_file_path = @cache_dir + pkg_file
131
+ raise(Errors::NoSuchPackageError.new(pkg_file.sub(/\.smcpak/, "")), "Package file '#{pkg_file}' not found in this repository's cache!") unless pkg_file_path.file?
132
+
133
+ directory.mktree unless directory.directory?
134
+
135
+ #No need to really "fetch" the package--this is a *local* repository
136
+ FileUtils.cp(pkg_file_path, directory)
137
+ directory + pkg_file
138
+ end
139
+
140
+ #Installs a package into this local repository in a way that SMC will find
141
+ #it’s contents.
142
+ #==Parameter
143
+ #[package] An instance of class Package. The package to install,
144
+ #==Example
145
+ # lr.install(a_package)
146
+ #==Remarks
147
+ #Ensure you have write permissions to the repository, otherwise
148
+ #you’ll get an Errno::EACCES exception from this method.
149
+ def install(package)
150
+ path = package.decompress(SmcGet.temp_dir) + package.spec.name
151
+
152
+ package.spec.save(@specs_dir)
153
+
154
+ FileUtils.cp_r(path.join(Package::LEVELS_DIR).children, @levels_dir)
155
+ FileUtils.cp_r(path.join(Package::MUSIC_DIR).children, @music_dir)
156
+ FileUtils.cp_r(path.join(Package::GRAPHICS_DIR).children, @graphics_dir)
157
+ FileUtils.cp_r(path.join(Package::SOUNDS_DIR).children, @sounds_dir)
158
+ FileUtils.cp_r(path.join(Package::WORLDS_DIR).children, @worlds_dir)
159
+
160
+ FileUtils.cp(package.path, @cache_dir)
161
+
162
+ @package_specs << package.spec #This package is now installed and therefore the spec must be in that array
163
+ end
164
+
165
+ #call-seq:
166
+ # uninstall(pkg_name)
167
+ # uninstall(pkg_name){|path| ...}
168
+ #
169
+ #Uninstalls a package by removing all files it owns. This method checks the
170
+ #checksums specified in the respective package specifications and if it
171
+ #detects a user-modified file, the given block is invoked. If the block
172
+ #evaluates to a truth value, the modified file is copied to a file
173
+ #in the same directory as the original with ".MODIFIED" just before the
174
+ #file extension. The blockless form always discards all modified files.
175
+ #==Parameters
176
+ #[pkg_name] The name of the package (without the .smcpak extension) to remove.
177
+ #[full_path] *Blockargument*. The full Pathname of a file that has been modified.
178
+ #==Example
179
+ # # Delete a package and save all modified files
180
+ # rr.uninstall("cool-world"){|file| true}
181
+ # # Delete a package and discard all modified files
182
+ # rr.uninstall("cool-world")
183
+ def uninstall(pkg_name)
184
+ spec = @package_specs.find{|spec| spec.name == pkg_name}
185
+
186
+ [:levels, :music, :sounds, :graphics].each do |sym|
187
+ contrib_dir = @path + self.class.const_get(:"CONTRIB_#{sym.upcase}_DIR")
188
+
189
+ #Delete all the files
190
+ files = spec[sym]
191
+ files.each do |filename|
192
+ full_path = contrib_dir + filename
193
+ #Check if the file was modified
194
+ if block_given? and Digest::SHA1.hexdigest(File.read(full_path)) != spec[:checksums][sym.to_s][filename] #to_s as the keys are strings there, see PackageSpecification.from_file
195
+ if yield(full_path) #Getting a truth value from the block means copying
196
+ FileUtils.cp(full_path, full_path.parent + filename.sub(/\.(.*?)$/, '.MODIFIED.\1'))
197
+ end
198
+ end
199
+ File.delete(full_path)
200
+ end
201
+
202
+ #Delete now empty directories
203
+ loop do
204
+ empty_dirs = []
205
+ contrib_dir.find do |path|
206
+ next if path == contrib_dir #We surely don’t want to delete the toplevel dir.
207
+ empty_dirs << path if path.directory? and path.children.empty?
208
+ end
209
+ #If no empty directories are present anymore, break out of the loop.
210
+ break if empty_dirs.empty?
211
+ #Otherwise delete the empty directories and redo the process, because
212
+ #the parent directories could be empty now.
213
+ empty_dirs.each{|path| File.delete(path)}
214
+ end
215
+ end
216
+
217
+ #Delete worlds as well. Worlds can’t reside in subdirectories, therefore it’s unnecessary
218
+ #to check for the empty-directory thing.
219
+ spec[:worlds].each do |dirname|
220
+ full_path = @worlds_dir + dirname
221
+ #Check if any of the world’s files has been modified
222
+ ["description.xml", "layer.xml", "world.xml"].each do |wfile|
223
+ full_wfile_path = full_path + wfile
224
+ if block_given? and Digest::SHA1.hexdigest(full_wfile_path) != spec[:checksums]["worlds"][dirname][wfile] #"worlds" is a string for technical reasons, see PackageSpecification.from_file
225
+ if yield(full_wfile_path) #Getting a truth value from the block means copying
226
+ FileUtils.cp_r(full_path, full_path.parent + "#{dirname}.MODIFIED")
227
+ break #Break from the inner iteration, we just need to copy once
228
+ end
229
+ end
230
+ end
231
+ FileUtils.rm_r(full_path)
232
+ end
233
+
234
+ File.delete(@specs_dir + spec.spec_file_name) #Remove the spec itself
235
+ @package_specs.delete(spec) #Otherwise we have a stale package in the array
236
+ end
237
+
238
+ #Returns the path this repository refers to.
239
+ def to_s
240
+ @path.to_s
241
+ end
242
+
243
+ def contain?(pkg)
244
+ if pkg.kind_of? Package
245
+ @package_specs.include?(pkg.spec)
246
+ else
247
+ @package_specs.any?{|spec| spec.name == pkg}
248
+ end
249
+ end
250
+ alias contains? contain?
251
+
252
+ def search(regexp, *attributes)
253
+ attributes << :name if attributes.empty? #Default value
254
+
255
+ @package_specs.each do |spec|
256
+ attributes.each do |att|
257
+ case att
258
+ when :name then yield(spec.name) if spec.name =~ regexp
259
+ when :title then yield(spec.name) if spec.title =~ regexp
260
+ when :authors then yield(spec.name) if spec.authors.any?{|a| a =~ regexp}
261
+ when :difficulty then yield(spec.name) if spec.difficulty =~ regexp
262
+ when :description then yield(spec.name) if spec.description =~ regexp
263
+ when :levels then yield(spec.name) if spec.levels.any?{|l| l =~ regexp}
264
+ when :music then yield(spec.name) if spec.music.any?{|m| m =~ regexp}
265
+ when :sounds then yield(spec.name) if spec.sound.any?{|s| s =~ regexp}
266
+ when :graphics then yield(spec.name) if spec.graphics.any?{|g| g =~ regexp}
267
+ when :worlds then yield(spec.name) if spec.worlds.any?{|w| w =~ regexp}
268
+ else
269
+ $stderr.puts("Warning: Unknown attribute #{att}, ignoring it.")
270
+ end #case
271
+ end #attributes.each
272
+ end # @package_specs.each
273
+ end #search
274
+
275
+ end #LocalRepository
276
+
277
+ end
@@ -1,279 +1,260 @@
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
- #An object of this class represents a package. Wheather it is installed or
24
- #not, doesn't matter (call #installed? to find out), but everything you
25
- #want to manage your packages can be found here. For example, to install
26
- #a remote package, do:
27
- # pkg = SmcGet::Package.new("mypackage")
28
- # pkg.install
29
- #Don't forget to set up smc-get before you use the library:
30
- # require "smc_get/"
31
- # SmcGet.setup(
32
- # "https://github.com/Luiji/Secret-Maryo-Chronicles-Contributed-Levels/raw/master/",
33
- # "dir/where/you/hava/smc/installed"
34
- # end
35
- class Package
36
-
37
- #The package specification file for this packages. This file may not
38
- #exist if the package is not installed. This is a Pathname object.
39
- attr_reader :spec_file
40
- #The name of this package.
41
- attr_reader :name
42
-
43
- class << self
44
-
45
- #Searches through the package repostitory and returns an array
46
- #of matching package objects.
47
- #
48
- #Pass in the regular expression to search for (or a string, which
49
- #then is treated as a regular expression without anchors), the
50
- #keys of the specification to search through as an array of symbols,
51
- #and wheather you want to query only locally installed packages (by
52
- #default, only remote packages are searched).+query_fields+ indicates
53
- #which fields of the package specification shall be searched. You can
54
- #pass them as an array of symbols. +only_local+ causes smc-get to
55
- #do a local search instead of a remote one.
56
- #
57
- #With solely :pkgname specified, just the specifications for the packages
58
- #whose package file names match +regexp+ are downloaded, causing a
59
- #massive speedup.
60
- def search(regexp, query_fields = [:pkgname], only_local = false)
61
- regexp = Regexp.new(Regexp.escape(regexp)) if regexp.kind_of? String
62
- ary = []
63
-
64
- list = if only_local
65
- Errors::LibraryNotInitialized.throw_if_needed!
66
- installed_packages.map(&:name)
67
- else
68
- Tempfile.open("smc-get") do |listfile|
69
- SmcGet.download(PACKAGE_LIST_FILE, listfile.path)
70
- listfile.readlines.map(&:chomp)
71
- end
72
- end
73
-
74
- list.each do |pkg_name|
75
- pkg = Package.new(pkg_name)
76
- #If the user wants to query just the pkgname, we can save
77
- #much time by not downloading all the package specs.
78
- if query_fields == [:pkgname]
79
- ary << pkg if pkg_name =~ regexp
80
- else
81
- spec = only_local ? pkg.spec : pkg.getinfo
82
- query_fields.each do |field|
83
- if field == :pkgname #This field is not _inside_ the spec.
84
- ary << pkg if pkg_name =~ regexp
85
- else
86
- #First to_s: Convert Symbol to string used in the specs.
87
- #Second to_s: Ensure array values such as "author" are
88
- # handled correctly.
89
- ary << pkg if spec[field.to_s].to_s =~ regexp
90
- end
91
- end
92
- end
93
- end
94
- ary
95
- end
96
-
97
- #Returns a list of all currently installed packages as an array of
98
- #Package objects.
99
- def installed_packages
100
- Errors::LibraryNotInitialized.throw_if_needed!
101
- specs_dir = SmcGet.datadir + PACKAGE_SPECS_DIR
102
- specs_dir.mkpath unless specs_dir.directory?
103
-
104
- #We need to chdir here, because Dir.glob returns the path
105
- #relative to the current working directory and it should be
106
- #a bit more performant if I don't have to strip off the relative
107
- #prefix of the filenames (which are the names of the packages + .yml).
108
- Dir.chdir(specs_dir.to_s) do
109
- Dir.glob("**/*.yml").map{|filename| new(filename.match(/\.yml$/).pre_match)}
110
- end
111
- end
112
-
113
- end
114
-
115
- #Creates a new package object from it's name. This doesn't do anything,
116
- #especially it doesn't install the package. It just creates an object for
117
- #you you can use to inspect or install pacakges. It doesn't even check if
118
- #the package name is valid.
119
- def initialize(package_name)
120
- Errors::LibraryNotInitialized.throw_if_needed!
121
- @name = package_name
122
- @spec_file = SmcGet.datadir.join(PACKAGE_SPECS_DIR, "#{@name}.yml")
123
- end
124
-
125
- # Install a package from the repository. Yields the name of the file
126
- # currently being downloaded, how many percent of that
127
- # file have already been downloaded and wheather or not this is a retry (if
128
- # so, the exception object is yielded, otherwise false).
129
- # The maximum number of retries is specified via the +max_tries+ parameter.
130
- def install(max_tries = 3) # :yields: file, percent_file, retrying
131
- try = 1 #For avoiding retrying infinitely
132
- begin
133
- SmcGet.download("packages/#{@name}.yml", SmcGet.datadir + PACKAGE_SPECS_DIR + "#{@name}.yml") do |file, percent_done|
134
- yield(file, percent_done, false) if block_given?
135
- end
136
- rescue Errors::DownloadFailedError => e
137
- if try > max_tries
138
- File.delete(SmcGet.datadir + PACKAGE_SPECS_DIR + "#{@name}.yml") #There is an empty file otherwise
139
- if e.class == Errors::ConnectionTimedOutError
140
- raise #Bubble up
141
- else
142
- raise(Errors::NoSuchPackageError.new(@name), "ERROR: Package not found in the repository: #{@name}.")
143
- end
144
- else
145
- try += 1
146
- yield(e.download_url, 0, e) if block_given?
147
- retry
148
- end
149
- end
150
-
151
- pkgdata = YAML.load_file(SmcGet.datadir + PACKAGE_SPECS_DIR + "#{@name}.yml")
152
- package_parts = [["levels", :level], ["music", :music], ["graphics", :graphic]]
153
-
154
- package_parts.each_with_index do |ary, i|
155
- part, sym = ary
156
- try = 1
157
-
158
- if pkgdata.has_key?(part)
159
- pkgdata[part].each do |filename|
160
- begin
161
- SmcGet.download("#{part}/#{filename}", SmcGet.datadir + SmcGet.const_get(:"PACKAGE_#{part.upcase}_DIR") + filename) do |file, percent_done|
162
- yield(file, percent_done, false) if block_given?
163
- end
164
- rescue Errors::DownloadFailedError => e
165
- if try > max_tries
166
- if e.class == Errors::ConnectionTimedOutError
167
- raise #Bubble up
168
- else
169
- raise(Errors::NoSuchResourceError.new(sym, error.download_url), "ERROR: #{sym.capitalize} not found in the repository: #{filename}.")
170
- end
171
- else
172
- try += 1
173
- yield(e.download_url, 0, e) if block_given?
174
- retry
175
- end #if try > max_tries
176
- end #begin
177
- end #each part
178
- end #has_key?
179
- end #each part and sym
180
- end #install
181
-
182
- # Uninstall a package from the local database. If a block is given,
183
- # it is yielded the package part currently being deleted and
184
- # how many percent of the files have already been deleted for the current package
185
- # part.
186
- def uninstall
187
- package_file = SmcGet.datadir + PACKAGE_SPECS_DIR + "#{@name}.yml"
188
- begin
189
- pkgdata = YAML.load_file(package_file)
190
- rescue Errno::ENOENT
191
- raise(Errors::NoSuchPackageError.new(@name), "ERROR: Local package not found: #{@name}.")
192
- end
193
-
194
- %w[music graphics levels].each_with_index do |part, part_index|
195
- if pkgdata.has_key? part
196
- total_files = pkgdata[part].count
197
- pkgdata[part].each_with_index do |filename, index|
198
- begin
199
- File.delete(SmcGet.datadir + SmcGet.const_get("PACKAGE_#{part.upcase}_DIR") + filename)
200
- rescue Errno::ENOENT
201
- end
202
- yield(part, ((index + 1) / total_files) * 100) if block_given? #+1, because index is 0-based
203
- end
204
- end
205
- end
206
-
207
- #Delete the package file
208
- File.delete(package_file)
209
-
210
- #Delete the directories the package file was placed in IF THEY'RE EMPTY.
211
- rel_dir, file = package_file.relative_path_from(SmcGet.datadir + PACKAGE_SPECS_DIR).split
212
- #rel_dir now holds the path difference between the package directory
213
- #and the package spec file. If it is ".", no further dirs have been
214
- #introduced.
215
- return if rel_dir == Pathname.new(".")
216
- #For simplifying the deletion procedure, we change the working directory
217
- #to the package spec dir. Otherwise we'd have to keep track of both
218
- #the absolute and relative paths, this way just of the latter.
219
- Dir.chdir(SmcGet.datadir.join(PACKAGE_SPECS_DIR).to_s) do
220
- #Remove from the inmost to the outmost directory, so that
221
- #empty directories contained in directories just containg that
222
- #empty directory don't get prohibited from deletion.
223
- rel_dir.ascend do |dir|
224
- dir.rmdir if dir.children.empty?
225
- end
226
- end
227
-
228
- end
229
-
230
- #Returns true if the package is installed locally. Returns false
231
- #otherwise.
232
- def installed?
233
- SmcGet.datadir.join(PACKAGE_SPECS_DIR, "#{@name}.yml").file?
234
- end
235
-
236
- #Get package information on a remote package. This method never
237
- #retrieves any information from a locally installed package, look
238
- #at #spec for that. Return value is the package specification in form
239
- #of a hash.
240
- #
241
- #WARNING: This function is not thread-safe.
242
- def getinfo
243
- yaml = nil
244
- Tempfile.open('pkgdata') do |tmp|
245
- begin
246
- SmcGet.download("packages/#{@name}.yml", tmp.path)
247
- rescue Errors::DownloadFailedError
248
- raise(Errors::NoSuchPackageError.new(@name), "ERROR: Package not found in the repository: #{@name}")
249
- end
250
- yaml = YAML.load_file(tmp.path)
251
- end
252
- return yaml
253
- end
254
-
255
- #Retrieves the package specification from a locally installed package
256
- #in form of a hash. In order to fetch information from a remote package,
257
- #you have to use the #getinfo method.
258
- def spec
259
- if installed?
260
- YAML.load_file(@spec_file)
261
- else
262
- raise(Errors::NoSuchPackageError, "ERROR: Package not installed locally: #@name.")
263
- end
264
- end
265
-
266
- #Returns the name of the package.
267
- def to_s
268
- @name
269
- end
270
-
271
- #Human-readable description of form
272
- # #<SmcGet::Package package_name (installation_status)>
273
- def inspect
274
- "#<#{self.class} #{@name} (#{installed? ? 'installed' : 'not installed'})>"
275
- end
276
-
277
- end
278
-
279
- 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
+ module SmcGet
21
+
22
+ #Packages are the main objects you will have to deal with. An instance of
23
+ #class Package encapsulates all known information from a smc package file
24
+ #(these files are described in the smcpak.rdoc file), and the most important
25
+ #attribute of this class is +spec+, which is your direct interface to the
26
+ #package’s specification by providing you with a fitting instance of class
27
+ #PackageSpecification.
28
+ #
29
+ #Apart from inspecting existing packages, you can also use the Package class
30
+ #to create new pacakges by calling the ::create method (inspecting a given
31
+ #package file is possible with ::from_file). ::create expects a path to
32
+ #the directoy which you want to compress into a new SMC package, validates
33
+ #it against the packaging guidelines and will then either fail or do
34
+ #the actual compression, outputting an instance of class Package that
35
+ #(you guessed it) describes the newly created SMC package.
36
+ #
37
+ #==Example of building a SMC package
38
+ #The following is an example that shows you how to build a basic
39
+ #SMC package by use of the ::create method.
40
+ #
41
+ #First you have to decide how you want to name your package. If you
42
+ #have decided (remember: This is a decision for life!), create a
43
+ #directory named after the package, note that it isn’t allowed to
44
+ #contain whitespace.
45
+ #
46
+ #Inside the directory, say you named it +cool+, create the following
47
+ #structure:
48
+ #
49
+ # cool/
50
+ # - cool.yml
51
+ # - README.txt
52
+ # levels/
53
+ # music/
54
+ # pixmaps/
55
+ # sounds/
56
+ # worlds/
57
+ #
58
+ #If your package doesn’t contain a specific component, e.g. sounds, you
59
+ #may ommit the corresponding directory.
60
+ #
61
+ #Then add all the levels you want to include in the package to the +levels+
62
+ #subdirectory, e.g. if you have a level named "awesome_1" and one named
63
+ #"awesome_2", copy them from your personal SMC directory (usually
64
+ #<b>~/.smc/levels</b>) so that your structure looks like this:
65
+ #
66
+ # cool/
67
+ # - cool.yml
68
+ # - README.txt
69
+ # levels/
70
+ # - awesome_1.smclvl
71
+ # - awesome_2.smclvl
72
+ # music/
73
+ # pixmaps/
74
+ # sounds/
75
+ # worlds/
76
+ #
77
+ #Now the most important step. Open up *cool.yml* in your favourite text
78
+ #editor and write the package specification. You can read about the exact
79
+ #format with all available options in the smcpak.rdoc file,but for now
80
+ #just write the following:
81
+ #
82
+ # ---
83
+ # title: Cool levels
84
+ # last_update: 01-01-2011 04:07:00Z
85
+ # levels:
86
+ # - awesome_1.smclvl
87
+ # - awesome_2.smclvl
88
+ # authors:
89
+ # - Your Name Here
90
+ # difficulty: medium
91
+ # description: |
92
+ # Here goes your description
93
+ # which may span multiple lines.
94
+ #
95
+ #Of course put something appropriate into the +last_update+ field
96
+ #(the format is DD-MM-YYYY hh:mm:ssZ, time zone is UTC).
97
+ #
98
+ #After you wrote something into your README.txt (whatever it is),
99
+ #you *could* build the package the easy way with
100
+ # $ cd /path/to/dir/above/cool
101
+ # $ smc-get build cool
102
+ #on the commandline. But I promised you to show the use of
103
+ #Package.create, so instead do:
104
+ # $ cd /path/to/dir/above/cool
105
+ # $ ruby -Ipath/to/smcget/lib -rsmc_get -e 'SmcGet::Package.create("cool")'
106
+ #Either way, you should now end up with a file called *cool.smcpak* in the
107
+ #parent directory of <b>cool/</b>.
108
+ class Package
109
+
110
+ #A package name is considered valid if it matches this Regular
111
+ #expression.
112
+ VALID_PKG_NAME = /^[a-zA-Z_\-0-9]+$/
113
+ #Name of the directory the levels reside in the package.
114
+ LEVELS_DIR = "levels"
115
+ #Name of the directory the music resides in the package.
116
+ MUSIC_DIR = "music"
117
+ #Name of the directory the sounds reside in the package.
118
+ SOUNDS_DIR = "sounds"
119
+ #Name of the dierctory the graphics reside in the pacakge.
120
+ GRAPHICS_DIR = "pixmaps"
121
+ #Name of the directory the worlds reside in the package.
122
+ WORLDS_DIR = "worlds"
123
+
124
+ #The PackageSpecification of this package.
125
+ attr_reader :spec
126
+ #The Pathname of the .smcpak file.
127
+ attr_reader :path
128
+
129
+ class << self
130
+
131
+ #Creates a new Package from a local .smcpak file.
132
+ #==Parameter
133
+ #[file] The path to the SMC package.
134
+ #==Return value
135
+ #An instance of class Package.
136
+ #==Example
137
+ # pkg = SmcGet::Package.from_file("/home/freak/mycoolpackage.smcpak")
138
+ #==Remarks
139
+ #As this needs to decompress the package temporarily, this method
140
+ #may take some time to complete.
141
+ def from_file(file)
142
+ pkg_name = File.basename(file).sub(/\.smcpak$/, "")
143
+ #No spec file is provided, we therefore need to extract it from
144
+ #the archive.
145
+ path = PackageArchive.new(file).decompress(SmcGet.temp_dir) + pkg_name + "#{pkg_name}.yml"
146
+ new(path, file)
147
+ end
148
+
149
+ #Validates +directory+ against the packaging guidelines and compresses it
150
+ #into a .smcpak file.
151
+ #==Parameter
152
+ #[directory] The path to the directory you want to compress.
153
+ #==Return value
154
+ #An instance of this class describing the newly created package.
155
+ #==Example
156
+ # pkg = SmcGet::Package.create("/home/freak/mycoollevels")
157
+ #==Remarks
158
+ #The .smcpak file is placed in the parent
159
+ #directory of +directory+, you should therefore ensure you have write
160
+ #permissions for it.
161
+ def create(directory)
162
+ #0. Determine the names of the important files
163
+ directory = Pathname.new(directory)
164
+ pkg_name = directory.basename.to_s
165
+ spec_file = directory + "#{pkg_name}.yml"
166
+ readme = directory + "README.txt"
167
+ smcpak_file = directory.parent + "#{pkg_name}.smcpak"
168
+
169
+ #1. Validate the package name
170
+ raise(Errors::BrokenPackageError, "Invalid package name!") unless pkg_name =~ VALID_PKG_NAME
171
+
172
+ #2. Validate the package spec
173
+ spec = PackageSpecification.from_file(spec_file) #Raises if necessary
174
+
175
+ #3. Validate the rest of the structure
176
+ $stderr.puts("Warning: No README.txt found.") unless readme.file?
177
+ $stderr.puts("Warning: No levels found.") if spec.levels.empty?
178
+
179
+ #4. Compress the whole thing
180
+ #The process is as follows: A temporary directory is created, in which
181
+ #a subdirectory that is named after the package is created. The
182
+ #spec, the README and the levels, music, etc. are then copied into
183
+ #that subdirectory which in turn is then compressed. The resulting
184
+ #.smcpak file is copied back to the original directory’s parent dir.
185
+ #After that, the mktmpdir block ends and deletes the temporary
186
+ #directory.
187
+ path = Dir.mktmpdir("smc-get-create-#{pkg_name}") do |tmpdir|
188
+ goal_dir = Pathname.new(tmpdir) + pkg_name
189
+ goal_dir.mkdir
190
+
191
+ FileUtils.cp(spec_file, goal_dir)
192
+ FileUtils.cp(readme, goal_dir) if readme.file? #Optional
193
+ [:levels, :graphics, :music, :sounds, :worlds].each do |sym|
194
+ #4.1. Create the group’s subdir
195
+ dirname = const_get(:"#{sym.upcase}_DIR")
196
+ goal_group_dir = goal_dir + dirname
197
+ goal_group_dir.mkdir
198
+ #4.2. Copy all the group’s files over to it
199
+ spec[sym].each do |filename|
200
+ FileUtils.cp(directory + dirname + filename, goal_group_dir)
201
+ end
202
+ end
203
+ #4.3. actual compression
204
+ PackageArchive.compress(goal_dir, smcpak_file).path
205
+ end
206
+ #5. Return a new instance of Package
207
+ from_file(path)
208
+ end
209
+
210
+ end
211
+
212
+ #Creates a new Package from the given specification file. You shouldn’t
213
+ #use this method directly, because you would duplicate the
214
+ #work the ::from_file method already does for you.
215
+ #==Parameters
216
+ #[spec_file] The path to the YAML specification file.
217
+ #[pkg_location] The path to the SMC package.
218
+ #==Return value
219
+ #The newly created Package instance.
220
+ #==Example
221
+ # pkg = SmcGet::Package.new("/home/freak/cool.yml", "/home/freak/cool.smcpak")
222
+ def initialize(spec_file, pkg_location)
223
+ @spec = PackageSpecification.from_file(spec_file)
224
+ @path = pkg_location
225
+ end
226
+
227
+ #Decompresses this package.
228
+ #==Parameter
229
+ #[directory] Where to extract the SMC package to. A subdirectory named after
230
+ # the package is automatically created in this directory.
231
+ #==Return value
232
+ #The Pathname to the created subdirectory.
233
+ #==Example
234
+ # pkg.decompress(".") #=> /home/freak/cool
235
+ def decompress(directory)
236
+ PackageArchive.new(@path).decompress(directory)
237
+ end
238
+
239
+ #Compares two packages. They’re considered equal if their package
240
+ #specifications are equal. See PackageSpecification#==.
241
+ def ==(other)
242
+ return false unless other.respond_to? :spec
243
+ @spec == other.spec
244
+ end
245
+
246
+ #Shorthand for:
247
+ # pkg.spec.name
248
+ def to_s
249
+ @spec.name
250
+ end
251
+
252
+ #Human-readabe description of form
253
+ # #<SmcGet::Package <package name>>
254
+ def inspect
255
+ "#<#{self.class} #{@spec.name}>"
256
+ end
257
+
258
+ end
259
+
260
+ end