smc-get 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
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
+ module CUICommands
24
+
25
+ class UninstallCommand < Command
26
+
27
+ def self.help
28
+ <<EOF
29
+ #{File.basename($0)} uninstall PACKAGES
30
+
31
+ Removes PACKAGES from your set of downloaded packages.
32
+ EOF
33
+ end
34
+
35
+ def self.summary
36
+ "uninstall\tUninstalls one or more packages."
37
+ end
38
+
39
+ def parse(args)
40
+ CUI.debug("Parsing #{args.count} args for uninstall.")
41
+ raise(InvalidCommandline, "No package given.") if args.empty?
42
+ @pkg_names = []
43
+ until args.empty?
44
+ arg = args.shift
45
+ #case arg
46
+ #when "-c", "--my-arg" then ...
47
+ #else
48
+ @pkg_names << arg
49
+ $stderr.puts("Unkown argument #{arg}. Treating it as a package.") if arg.start_with?("-")
50
+ #end
51
+ end
52
+ end
53
+
54
+ def execute(config)
55
+ CUI.debug("Executing uninstall.")
56
+ @pkg_names.each do |pkg_name|
57
+ pkg = Package.new(pkg_name)
58
+ puts "Uninstalling #{pkg}."
59
+ unless pkg.installed?
60
+ $stderr.puts "#{pkg} is not installed. Skipping."
61
+ next
62
+ end
63
+ #Windows doesn't understand ANSI escape sequences, so we have to
64
+ #use the carriage return and reprint the whole line.
65
+ base_str = "\rRemoving %s... (%.2f%%)"
66
+ pkg.uninstall do |part, percent_part|
67
+ print "\r", " " * 80 #Clear everything written before
68
+ printf(base_str, part, percent_part)
69
+ end
70
+ puts
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
78
+ end
@@ -0,0 +1,57 @@
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
+ module CUICommands
24
+
25
+ class VersionCommand < Command
26
+
27
+ def self.help
28
+ <<EOF
29
+ USAGE: #{File.basename($0)} version
30
+
31
+ Shows the version of #{File.basename($0)} and the copyright statement.
32
+ EOF
33
+ end
34
+
35
+ def self.summary
36
+ "version\tShows smc-get's version and copyright."
37
+ end
38
+
39
+ def parse(args)
40
+ raise(InvalidCommandline, "Too many arguments.") unless args.empty?
41
+ end
42
+
43
+ def execute(config)
44
+ puts "This is #{File.basename($0)}, version #{VERSION}."
45
+ puts
46
+ puts "#{File.basename($0)} Copyright (C) 2010-2011 Luiji Maryo"
47
+ puts "#{File.basename($0)} Copyright (C) 2011 Marvin Gülker"
48
+ puts "This program comes with ABSOLUTELY NO WARRANTY."
49
+ puts "This is free software, and you are welcome to redistribute it"
50
+ puts "under certain conditions; see the COPYING file for information."
51
+ end
52
+
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,113 @@
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 module contains all errors messages that are specific to smc-get.
24
+ module Errors
25
+
26
+ #Superclass for all errors in this library.
27
+ class SmcGetError < StandardError
28
+ end
29
+
30
+ #Raises when you did not call SmcGet.setup.
31
+ class LibraryNotInitialized < SmcGetError
32
+
33
+ #Throws an exception of this class with an appropriate error
34
+ #message if smc-get has not been initialized correctly.
35
+ def self.throw_if_needed!
36
+ if SmcGet.datadir.nil? or SmcGet.repo_url.nil?
37
+ raise(self, "You have to setup smc-get first!")
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ # Raised when the class is initialized with a non-existant settings file.
44
+ class CannotFindSettings < SmcGetError
45
+ # The path to the settings file that was specified.
46
+ attr_reader :settings_path
47
+
48
+ # Create a new instance of the exception with the settings path.
49
+ def initialize(settings_path)
50
+ @settings_path = settings_path
51
+ end
52
+ end
53
+
54
+ # Raised when a package call is made but the specified package cannot be
55
+ # found.
56
+ class NoSuchPackageError < SmcGetError
57
+ # The name of the package that could not be found.
58
+ attr_reader :package_name
59
+
60
+ # Create a new instance of the exception with the specified package name.
61
+ def initialize(name)
62
+ @package_name = name
63
+ end
64
+ end
65
+
66
+ # Raised when a package call is made but one of the resources of the
67
+ # specified package is missing.
68
+ class NoSuchResourceError < SmcGetError
69
+ # The type of resource (should be either :music, :graphic, or :level).
70
+ attr_reader :resource_type
71
+ # The name of the resource (i.e. mylevel.lvl or Stuff/Cheeseburger.png).
72
+ attr_reader :resource_name
73
+
74
+ # Create a new instance of the exception with the specified resource type
75
+ # and name. Type should either be :music, :graphic, or :level.
76
+ def initialize(type, name)
77
+ @resource_type = type
78
+ @resource_name = name
79
+ end
80
+
81
+ # Returns true if the resource type is :music. False otherwise.
82
+ def is_music?
83
+ @resource_type == :music
84
+ end
85
+
86
+ # Returns true if the resource type is :graphic. False otherwise.
87
+ def is_graphic?
88
+ @resource_type == :graphic
89
+ end
90
+
91
+ # Returns true if the resource type is :level. False otherwise.
92
+ def is_level?
93
+ @resource_type == :level
94
+ end
95
+ end
96
+
97
+ # Raised when a call to download() fails.
98
+ class DownloadFailedError < SmcGetError
99
+ # The URL that failed to download (including everything after /raw/master
100
+ # only).
101
+ attr_reader :download_url
102
+
103
+ def initialize(url)
104
+ @download_url = url
105
+ end
106
+ end
107
+
108
+ #Raised when SmcGet.download timed out.
109
+ class ConnectionTimedOutError < DownloadFailedError
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,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
+ ################################################################################
@@ -0,0 +1,279 @@
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