smc-get 0.1.0 → 0.2.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.
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