sakai-info 0.1.0

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