smc-get 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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