sakai-info 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,35 @@
1
+ # sakai-info/cli.rb
2
+ # - sakai-info command line tool support
3
+ #
4
+ # Created 2012-02-19 daveadams@gmail.com
5
+ # Last updated 2012-02-19 daveadams@gmail.com
6
+ #
7
+ # https://github.com/daveadams/sakai-info
8
+ #
9
+ # This software is public domain.
10
+ #
11
+
12
+ require 'sakai-info/cli/help'
13
+
14
+ module SakaiInfo
15
+ class CLI
16
+ def self.validate_config
17
+ return true if Configuration.configured?
18
+
19
+ begin
20
+ Configuration.load_config
21
+ return true
22
+ rescue NoConfigFoundException
23
+ STDERR.puts "ERROR: No configuration file was found at #{Configuration::DEFAULT_CONFIG_FILE}"
24
+ return false
25
+ rescue InvalidConfigException => e
26
+ STDERR.puts "ERROR: Configuration was invalid:"
27
+ e.message.each_line do |line|
28
+ STDERR.puts " #{line.chomp}"
29
+ end
30
+ return false
31
+ end
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,103 @@
1
+ # sakai-info/cli/help.rb
2
+ # - sakai-info command line help
3
+ #
4
+ # Created 2012-02-19 daveadams@gmail.com
5
+ # Last updated 2012-02-19 daveadams@gmail.com
6
+ #
7
+ # https://github.com/daveadams/sakai-info
8
+ #
9
+ # This software is public domain.
10
+ #
11
+
12
+ module SakaiInfo
13
+ class CLI
14
+ class Help
15
+ STRINGS = {
16
+ :default => <<EOF,
17
+ sakai-info #{VERSION}
18
+
19
+ Object commands:
20
+ user Print information about a user or users
21
+ site Print information about a site or sites
22
+
23
+ Misc commands:
24
+ validate Validates configuration
25
+ help Prints general help
26
+ version Prints version
27
+
28
+ Type 'sakai-info help <command>' for help on a specific command.
29
+ EOF
30
+
31
+ "help" => <<EOF,
32
+ sakai-info help
33
+
34
+ Prints usage information for other sakai-info commands, or without an
35
+ argument it prints a list of possible commands.
36
+
37
+ Usage: sakai-info help [<command>]
38
+ EOF
39
+
40
+ "version" => <<EOF,
41
+ sakai-info version
42
+
43
+ Prints the current version of sakai-info.
44
+
45
+ Usage: sakai-info version
46
+ EOF
47
+
48
+ "validate" => <<EOF,
49
+ sakai-info validate
50
+
51
+ Reads and validates the current configuration format. To test the actual
52
+ database connections, use 'sakai-info test'.
53
+
54
+ Usage: sakai-info validate
55
+ EOF
56
+
57
+ "test" => <<EOF,
58
+ sakai-info test
59
+
60
+ [NOT YET IMPLEMENTED]
61
+
62
+ Reads configuration and tests connecting to each database specified, or with
63
+ an argument, it will test only the named instance.
64
+
65
+ Usage: sakai-info test [<instance>]
66
+ EOF
67
+
68
+ "user" => <<EOF,
69
+ sakai-info user
70
+
71
+ In this release, this command prints the total number of records in the
72
+ SAKAI_USER table. In future releases, subcommands will be available to
73
+ perform more tasks.
74
+
75
+ Usage: sakai-info user [<subcommand>]
76
+ EOF
77
+
78
+ "site" => <<EOF,
79
+ sakai-info site
80
+
81
+ In this release, this command prints the total number of records in the
82
+ SAKAI_SITE table. In future releases, subcommands will be available to
83
+ perform more tasks.
84
+
85
+ Usage: sakai-info site [<subcommand>]
86
+ EOF
87
+ }
88
+
89
+ def self.help(topic = :default, io = STDOUT)
90
+ topic ||= :default
91
+ if STRINGS.has_key? topic
92
+ io.puts STRINGS[topic]
93
+ else
94
+ STDERR.puts "ERROR: help topic '#{topic}' was unrecognized"
95
+ STDERR.puts
96
+ CLI.help(:default, STDERR)
97
+ exit 1
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
@@ -0,0 +1,288 @@
1
+ # sakai-info/configuration.rb
2
+ # SakaiInfo::Configuration library
3
+ #
4
+ # Created 2012-02-15 daveadams@gmail.com
5
+ # Last updated 2012-02-19 daveadams@gmail.com
6
+ #
7
+ # https://github.com/daveadams/sakai-info
8
+ #
9
+ # This software is public domain.
10
+ #
11
+
12
+ module SakaiInfo
13
+ class NoConfigFoundException < SakaiException; end
14
+ class AlreadyConfiguredException < SakaiException; end
15
+ class InvalidInstanceNameException < SakaiException; end
16
+ class InvalidConfigException < SakaiException; end
17
+ class UnsupportedConfigException < InvalidConfigException; end
18
+ class MultipleConfigException < InvalidConfigException
19
+ def initialize
20
+ @exceptions = []
21
+ end
22
+
23
+ def add(instance_name, exception)
24
+ @exceptions << [instance_name, exception]
25
+ end
26
+
27
+ def count
28
+ @exceptions.length
29
+ end
30
+
31
+ def message
32
+ "Multiple config exceptions were found:\n " +
33
+ @exceptions.collect{ |e| "#{e[0]}: #{e[1].message}" }.join("\n ")
34
+ end
35
+ end
36
+
37
+ # config format assumptions:
38
+ # a YAML string containing one of the following options:
39
+ # - a single database connection spec
40
+ # - eg:
41
+ # ---
42
+ # dbtype: oracle
43
+ # dbsid: PROD
44
+ # username: sakai
45
+ # password: Sekrit1
46
+ # OR:
47
+ # - an "instances" key containing a hash of named connection specs
48
+ # - with a "default" key naming the spec to use if none is specified
49
+ # - eg:
50
+ # ---
51
+ # default: production
52
+ # instances:
53
+ # production:
54
+ # dbtype: oracle
55
+ # service: PROD
56
+ # username: sakai
57
+ # password: prodpass
58
+ # test:
59
+ # dbtype: oracle
60
+ # service: TEST
61
+ # host: testdb.host
62
+ # port: 1521
63
+ # username: sakai
64
+ # password: testpass
65
+ # dev:
66
+ # dbtype: mysql
67
+ # host: dbserver.hostname.int
68
+ # port: 3306
69
+ # username: sakai
70
+ # password: ironchef
71
+ # dbname: sakaidev
72
+ # localdev:
73
+ # dbtype: mysql
74
+ # host: localhost
75
+ # port: 3306
76
+ # username: sakai
77
+ # password: ironchef
78
+ # dbname: sakailocal
79
+ #
80
+ #
81
+ # NOTES:
82
+ # - Oracle connections should use either an alias defined in the
83
+ # driver's tnsnames.ora file, or specify the host and port as the
84
+ # test instance example above does
85
+ #
86
+ class Configuration
87
+ @@config = nil
88
+
89
+ # validate just a single database connection configuration hash
90
+ def self.validate_single_connection_config(config)
91
+ if config.nil?
92
+ raise InvalidConfigException.new("The config provided was nil")
93
+ end
94
+
95
+ if not config.is_a? Hash
96
+ raise InvalidConfigException.new("The config provided must be a Hash")
97
+ end
98
+
99
+ # we have to have a dbtype value or we can't validate
100
+ if config["dbtype"].nil?
101
+ raise InvalidConfigException.new("The config does not specify 'dbtype'")
102
+ end
103
+
104
+ # force lowercase to simplify comparisons
105
+ dbtype = config["dbtype"].downcase
106
+
107
+ if not %w(oracle mysql).include? dbtype
108
+ raise UnsupportedConfigException.new("Database type '#{dbtype}' is not supported.")
109
+ end
110
+
111
+ # now check per-dbtype requirements
112
+ if dbtype == "oracle"
113
+ %w(service username password).each do |required_key|
114
+ if config[required_key].nil? or config[required_key] == ""
115
+ raise InvalidConfigException.new("Oracle config requires values for 'service', 'username', and 'password'.")
116
+ end
117
+ end
118
+ elsif dbtype == "mysql"
119
+ %w(host username password dbname).each do |required_key|
120
+ if config[required_key].nil? or config[required_key] == ""
121
+ raise InvalidConfigException.new("MySQL config requires values for 'host', 'username', 'password', and 'dbname'.")
122
+ end
123
+ end
124
+ else
125
+ # we should never have made it here
126
+ raise UnsupportedConfigException.new("Database type '#{dbtype}' is not supported.")
127
+ end
128
+
129
+ # for both types, 'port' is optional but if it exists it must be a valid TCP port
130
+ if not config["port"].nil?
131
+ begin
132
+ port = config["port"].to_i
133
+ if port < 1 or port > 65535
134
+ raise
135
+ end
136
+ rescue
137
+ raise InvalidConfigException.new("Config value 'port' must be a valid TCP port number.")
138
+ end
139
+ end
140
+
141
+ # if we made it here the config is complete and well-formed
142
+ return true
143
+ end
144
+
145
+ # validate that configuration is complete and well-formed
146
+ def self.validate_config(config)
147
+ if config.nil?
148
+ raise InvalidConfigException.new("The config provided was nil")
149
+ end
150
+
151
+ if not config.is_a? Hash
152
+ raise InvalidConfigException.new("The config provided must be a Hash")
153
+ end
154
+
155
+ # if 'dbtype' exists, it will be a single database configuration
156
+ if config["dbtype"]
157
+ self.validate_single_connection_config(config)
158
+ else
159
+ # otherwise both 'default' and 'instances' keys are required
160
+ if config.keys.sort != ["default","instances"]
161
+ raise InvalidConfigException.new("The config must specify either 'dbtype' or both 'default' and 'instances'.")
162
+ end
163
+
164
+ # enforce types on the values of 'default' and 'instances'
165
+ if not config["default"].is_a? String
166
+ raise InvalidConfigException.new("The value of 'default' must be a String.")
167
+ end
168
+ if not config["instances"].is_a? Hash
169
+ raise InvalidConfigException.new("The value of 'instances' must be a Hash.")
170
+ end
171
+
172
+ # 'default' must be a string pointing to one of the 'instances' keys
173
+ if not config["instances"].keys.include? config["default"]
174
+ raise InvalidConfigException.new("The default instance '#{config["default"]}' was not among the instance names given: #{config["instances"].keys.inspect}")
175
+ end
176
+
177
+ # check the validity of each instance, collecting exceptions as we go
178
+ multi_exceptions = nil
179
+ config["instances"].keys.each do |instance_name|
180
+ begin
181
+ self.validate_single_connection_config(config["instances"][instance_name])
182
+ rescue InvalidConfigException => e
183
+ # create the object if it doesn't already exist
184
+ multi_exceptions ||= MultipleConfigException.new
185
+
186
+ # add this exception to the list
187
+ multi_exceptions.add(instance_name, e)
188
+ end
189
+ # continue the loop no matter what
190
+ end
191
+
192
+ # if this object was created, the exception needs to be raised
193
+ if not multi_exceptions.nil?
194
+ raise multi_exceptions
195
+ end
196
+ end
197
+
198
+ # if we've made it this far, the configuration must be fine
199
+ return true
200
+ end
201
+
202
+ def initialize(config)
203
+ begin
204
+ if config.is_a? Hash
205
+ @config = config
206
+ elsif config.is_a? String and File.exist?(config)
207
+ # try to parse as a filename first
208
+ if File.exist?(config)
209
+ @config = YAML::load_file(config)
210
+ end
211
+ else
212
+ # otherwise try to parse it generically
213
+ @config = YAML::load(config)
214
+ end
215
+ rescue Exception => e
216
+ raise InvalidConfigException.new("Unable to parse configuration: #{e}")
217
+ end
218
+
219
+ # check that the configuration specified is well formed
220
+ if not Configuration.validate_config(@config)
221
+ raise InvalidConfigException.new("Config provided is either incomplete or poorly formed.")
222
+ end
223
+
224
+ # create instance objects
225
+ @instances = {}
226
+ if not @config["instances"].nil?
227
+ @config["instances"].keys.each do |instance_name|
228
+ @instances[instance_name] = Instance.create(@config["instances"][instance_name])
229
+ if instance_name == @config["default"]
230
+ @instances[:default] = @instances[instance_name]
231
+ end
232
+ end
233
+ else
234
+ @instances[:default] = Instance.create(@config)
235
+ end
236
+ end
237
+
238
+ # instance accessibility
239
+ def get_instance(instance_name)
240
+ @instances[instance_name] or raise InvalidInstanceNameException
241
+ end
242
+
243
+ def default_instance
244
+ @instances[:default]
245
+ end
246
+
247
+ DEFAULT_CONFIG_FILE = File.expand_path("~/.sakai-info")
248
+ # check to see if configuration file exists
249
+ # by default ~/.sakai-info
250
+ def self.config_file_path
251
+ if File.readable? DEFAULT_CONFIG_FILE
252
+ DEFAULT_CONFIG_FILE
253
+ else
254
+ nil
255
+ end
256
+ end
257
+
258
+ # are we already configured?
259
+ def self.configured?
260
+ not @@config.nil?
261
+ end
262
+
263
+ # load configuration as a class variable (and return it as well)
264
+ def self.load_config(alternate_config_file = nil)
265
+ if Configuration.configured?
266
+ raise AlreadyConfiguredException
267
+ end
268
+
269
+ unless(config_file = alternate_config_file || Configuration.config_file_path)
270
+ raise NoConfigFoundException
271
+ end
272
+
273
+ @@config = Configuration.new(config_file)
274
+ end
275
+
276
+ # return specified database connection configuration
277
+ def self.get_instance(instance_name = nil)
278
+ Configuration.load_config unless Configuration.configured?
279
+
280
+ if instance_name.nil?
281
+ @@config.default_instance
282
+ else
283
+ @@config.get_instance(instance_name)
284
+ end
285
+ end
286
+ end
287
+ end
288
+
@@ -0,0 +1,300 @@
1
+ # sakai-info/content.rb
2
+ # SakaiInfo::Content library
3
+ #
4
+ # Created 2012-02-17 daveadams@gmail.com
5
+ # Last updated 2012-02-18 daveadams@gmail.com
6
+ #
7
+ # https://github.com/daveadams/sakai-info
8
+ #
9
+ # This software is public domain.
10
+ #
11
+
12
+ module SakaiInfo
13
+ class Content < SakaiObject
14
+ attr_reader :parent_id
15
+
16
+ def self.find(id)
17
+ begin
18
+ ContentResource.find(id)
19
+ rescue ObjectNotFoundException
20
+ begin
21
+ ContentCollection.find(id)
22
+ rescue ObjectNotFoundException
23
+ if not id.match(/\/$/)
24
+ ContentCollection.find(id + "/")
25
+ else
26
+ raise ObjectNotFoundException.new(Content, id)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def parent
33
+ if @parent_id.nil?
34
+ nil
35
+ else
36
+ @parent ||= ContentCollection.find(@parent_id)
37
+ end
38
+ end
39
+
40
+ def binary
41
+ if @binary.nil?
42
+ DB.connect.exec("select binary_entity from #{@table_name} " +
43
+ "where #{@id_column} = :id", id) do |row|
44
+ @binary = row[0].read
45
+ end
46
+ end
47
+ @binary
48
+ end
49
+
50
+ def size_on_disk
51
+ 0
52
+ end
53
+
54
+ def default_serialization
55
+ {
56
+ "id" => self.id,
57
+ "parent" => self.parent_id,
58
+ "size_on_disk" => self.size_on_disk
59
+ }
60
+ end
61
+
62
+ def summary_serialization
63
+ {
64
+ "id" => self.id,
65
+ "parent" => self.parent_id,
66
+ "size_on_disk" => self.size_on_disk
67
+ }
68
+ end
69
+
70
+ def realm
71
+ if @realm_is_nil
72
+ nil
73
+ else
74
+ begin
75
+ @realm ||= AuthzRealm.find_by_name("/content#{@id}")
76
+ rescue AuthzRealmNotFoundException
77
+ @realm_is_nil = true
78
+ nil
79
+ end
80
+ end
81
+ end
82
+
83
+ def effective_realm
84
+ self.realm || (parent.nil? ? nil : parent.effective_realm)
85
+ end
86
+ end
87
+
88
+ class ContentResource < Content
89
+ attr_reader :file_path, :uuid, :context, :resource_type_id
90
+
91
+ def initialize(id, parent_id, file_path, uuid, file_size, context, resource_type_id)
92
+ @id = id
93
+ @parent_id = parent_id
94
+ @file_path = File.join(Instance.content_base_directory, file_path)
95
+ @uuid = uuid
96
+ @file_size = file_size
97
+ @context = context
98
+ @resource_type_id = resource_type_id
99
+
100
+ @table_name = "content_resource"
101
+ @id_column = "resource_id"
102
+ end
103
+
104
+ @@cache = {}
105
+ def self.find(id)
106
+ if @@cache[id].nil?
107
+ DB.connect.exec("select resource_id, in_collection, file_path, " +
108
+ "resource_uuid, file_size, context, resource_type_id " +
109
+ "from content_resource " +
110
+ "where resource_id=:id", id) do |row|
111
+ @@cache[id] = ContentResource.new(row[0], row[1], row[2], row[3], row[4].to_i, row[5], row[6])
112
+ end
113
+ if @@cache[id].nil?
114
+ raise ObjectNotFoundException.new(ContentResource, id)
115
+ end
116
+ end
117
+ @@cache[id]
118
+ end
119
+
120
+ def size_on_disk
121
+ @file_size
122
+ end
123
+
124
+ def default_serialization
125
+ {
126
+ "id" => self.id,
127
+ "parent" => self.parent_id,
128
+ "uuid" => self.uuid,
129
+ "file_path" => self.file_path,
130
+ "size_on_disk" => self.size_on_disk,
131
+ "context" => self.context,
132
+ "resource_type_id" => self.resource_type_id,
133
+ "effective_realm" => (self.effective_realm.nil? ? nil : self.effective_realm.name)
134
+ }
135
+ end
136
+
137
+ def self.find_by_parent(parent_id)
138
+ resources = []
139
+ DB.connect.exec("select resource_id, in_collection, file_path, " +
140
+ "resource_uuid, file_size, context, resource_type_id " +
141
+ "from content_resource " +
142
+ "where in_collection=:parent_id", parent_id) do |row|
143
+ @@cache[row[0]] = ContentResource.new(row[0], row[1], row[2], row[3], row[4].to_i, row[5], row[6])
144
+ resources << @@cache[row[0]]
145
+ end
146
+ resources
147
+ end
148
+
149
+ def self.count_by_parent(parent_id)
150
+ count = 0
151
+ DB.connect.exec("select count(*) from content_resource " +
152
+ "where in_collection=:parent_id", parent_id) do |row|
153
+ count = row[0].to_i
154
+ end
155
+ count
156
+ end
157
+ end
158
+
159
+ class ContentCollection < Content
160
+ def initialize(id, parent_id)
161
+ @id = id
162
+ @parent_id = parent_id
163
+
164
+ @table_name = "content_collection"
165
+ @id_column = "collection_id"
166
+ end
167
+
168
+ @@cache = {}
169
+ def self.find(id)
170
+ if id !~ /\/$/
171
+ id += "/"
172
+ end
173
+
174
+ if @@cache[id].nil?
175
+ DB.connect.exec("select collection_id, in_collection from content_collection " +
176
+ "where collection_id=:id", id) do |row|
177
+ @@cache[id] = ContentCollection.new(row[0], row[1])
178
+ end
179
+ if @@cache[id].nil?
180
+ raise ObjectNotFoundException.new(ContentCollection, id)
181
+ end
182
+ end
183
+ @@cache[id]
184
+ end
185
+
186
+ # return a content collection, even if it's a missing one
187
+ def self.find!(id)
188
+ begin
189
+ ContentCollection.find(id)
190
+ rescue ObjectNotFoundException
191
+ MissingContentCollection.find(id)
192
+ end
193
+ end
194
+
195
+ def self.find_portfolio_interaction_collections
196
+ collections = []
197
+ DB.connect.exec("select collection_id, in_collection from content_collection " +
198
+ "where collection_id like '%/portfolio-interaction/'") do |row|
199
+ @@cache[row[0]] = ContentCollection.new(row[0], row[1])
200
+ collections << @@cache[row[0]]
201
+ end
202
+ collections
203
+ end
204
+
205
+ def self.count_by_parent(parent_id)
206
+ count = 0
207
+ DB.connect.exec("select count(*) from content_collection " +
208
+ "where in_collection=:parent_id", parent_id) do |row|
209
+ count = row[0].to_i
210
+ end
211
+ count
212
+ end
213
+
214
+ def size_on_disk
215
+ if @size_on_disk.nil?
216
+ DB.connect.exec("select sum(file_size) from content_resource " +
217
+ "where resource_id like :collmatch", (@id + "%")) do |row|
218
+ @size_on_disk = row[0].to_i
219
+ end
220
+ end
221
+ @size_on_disk
222
+ end
223
+
224
+ def children
225
+ @child_collections ||= ContentCollection.find_by_parent(@id)
226
+ @child_resources ||= ContentResource.find_by_parent(@id)
227
+ {
228
+ "collections" => @child_collections,
229
+ "resources" => @child_resources
230
+ }
231
+ end
232
+
233
+ def child_counts
234
+ @child_collection_count ||= if @child_collections.nil?
235
+ ContentCollection.count_by_parent(@id)
236
+ else
237
+ @child_collections.length
238
+ end
239
+ @child_resource_count ||= if @child_resources.nil?
240
+ ContentResource.count_by_parent(@id)
241
+ else
242
+ @child_resources.length
243
+ end
244
+ {
245
+ "collections" => @child_collection_count,
246
+ "resources" => @child_resource_count,
247
+ "total" => @child_collection_count + @child_resource_count
248
+ }
249
+ end
250
+
251
+ def default_serialization
252
+ {
253
+ "id" => self.id,
254
+ "parent" => self.parent_id,
255
+ "size_on_disk" => self.size_on_disk,
256
+ "children" => self.child_counts,
257
+ "effective_realm" => (self.effective_realm.nil? ? nil : effective_realm.name)
258
+ }
259
+ end
260
+
261
+ def summary_serialization
262
+ {
263
+ "id" => self.id,
264
+ "parent" => self.parent_id
265
+ }
266
+ end
267
+
268
+ def children_serialization
269
+ {
270
+ "collections" => self.children["collections"].collect { |cc|
271
+ cc.serialize(:summary, :children)
272
+ },
273
+ "resources" => self.children["resources"].collect { |cr|
274
+ cr.serialize(:summary)
275
+ }
276
+ }
277
+ end
278
+
279
+ def self.find_by_parent(parent_id)
280
+ collections = []
281
+ DB.connect.exec("select collection_id, in_collection from content_collection " +
282
+ "where in_collection = :parent_id", parent_id) do |row|
283
+ @@cache[row[0]] = ContentCollection.new(row[0], row[1])
284
+ collections << @@cache[row[0]]
285
+ end
286
+ collections
287
+ end
288
+ end
289
+
290
+ class MissingContentCollection < ContentCollection
291
+ @@cache = {}
292
+ def self.find(id)
293
+ @@cache[id] ||= MissingContentCollection.new(id, nil)
294
+ end
295
+
296
+ def size_on_disk
297
+ 0
298
+ end
299
+ end
300
+ end