octopus 0.0.1
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/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +41 -0
- data/Rakefile +62 -0
- data/VERSION +1 -0
- data/bin/octopus +16 -0
- data/doc/octopus.zargo +0 -0
- data/lib/ext/array_ext.rb +8 -0
- data/lib/ext/partials.rb +27 -0
- data/lib/ext/reloader.rb +11 -0
- data/lib/init.rb +68 -0
- data/lib/octopus/config.ru +11 -0
- data/lib/octopus/grabbers/generic_http.rb +75 -0
- data/lib/octopus/models/net_resource.rb +68 -0
- data/lib/octopus/models/subscription.rb +35 -0
- data/lib/octopus/public/default.css +233 -0
- data/lib/octopus/public/images/bg01.jpg +0 -0
- data/lib/octopus/public/images/bg02.jpg +0 -0
- data/lib/octopus/public/images/bg03.jpg +0 -0
- data/lib/octopus/public/images/bg04.jpg +0 -0
- data/lib/octopus/public/images/img01.jpg +0 -0
- data/lib/octopus/public/images/img02.gif +0 -0
- data/lib/octopus/public/images/img03.gif +0 -0
- data/lib/octopus/public/images/img04.gif +0 -0
- data/lib/octopus/public/images/img05.gif +0 -0
- data/lib/octopus/public/images/img06.jpg +0 -0
- data/lib/octopus/public/images/spacer.gif +0 -0
- data/lib/octopus/views/edit.erb +29 -0
- data/lib/octopus/views/index.erb +21 -0
- data/lib/octopus/views/layout.erb +65 -0
- data/lib/octopus/views/new.erb +42 -0
- data/lib/octopus/views/resource_list_item.erb +9 -0
- data/lib/octopus.rb +81 -0
- data/octopus.gemspec +113 -0
- data/test/helper.rb +50 -0
- data/test/support/blueprints.rb +20 -0
- data/test/test_net_resource.rb +79 -0
- data/test/test_octopus.rb +153 -0
- data/test/test_subscription.rb +45 -0
- metadata +231 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 dave@boomer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
= octopus
|
2
|
+
|
3
|
+
This thing is an experimental octopus implementation.
|
4
|
+
|
5
|
+
== Installation
|
6
|
+
|
7
|
+
|
8
|
+
sudo apt-get install libsqlite3-ruby libsqlite3-dev ruby rubygems
|
9
|
+
sudo gem install octopus
|
10
|
+
|
11
|
+
*Grab the code:*
|
12
|
+
|
13
|
+
Anonymous checkout:
|
14
|
+
|
15
|
+
foo: put github stuff here
|
16
|
+
|
17
|
+
Commit access:
|
18
|
+
|
19
|
+
git clone git@escapegoat.org:octopus.git
|
20
|
+
|
21
|
+
Once you've got the code, just run the octopus like so:
|
22
|
+
|
23
|
+
ruby octopus.rb
|
24
|
+
|
25
|
+
Then visit http://localhost:4567 and you can start adding resources for your octopus to grab. There is a resource creation form at http://localhost:4567/new
|
26
|
+
|
27
|
+
|
28
|
+
== Note on Patches/Pull Requests
|
29
|
+
|
30
|
+
* Fork the project.
|
31
|
+
* Make your feature addition or bug fix.
|
32
|
+
* Add tests for it. This is important so I don't break it in a
|
33
|
+
future version unintentionally.
|
34
|
+
* Commit, do not mess with rakefile, version, or history.
|
35
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
36
|
+
* Send me a pull request. Bonus points for topic branches.
|
37
|
+
|
38
|
+
== Copyright
|
39
|
+
|
40
|
+
Copyright (c) 2009 dave@boomer See LICENSE for details.
|
41
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "octopus"
|
8
|
+
gem.summary = %Q{An experimental octopus implementation.}
|
9
|
+
gem.description = %Q{Grabs stuff off the net and notifies interested subscribers.}
|
10
|
+
gem.email = "dave.hrycyszyn@headlondon.com"
|
11
|
+
gem.homepage = "http://github.com/futurechimp/octopus"
|
12
|
+
gem.authors = ["dave@boomer"]
|
13
|
+
gem.add_dependency "datamapper", ">= 0.10.1"
|
14
|
+
gem.add_dependency "do_sqlite3", ">= 0.10.0"
|
15
|
+
gem.add_dependency "sinatra", ">= 0.9.4"
|
16
|
+
gem.add_dependency "thin", ">= 1.2.5"
|
17
|
+
gem.add_dependency "rack-flash", ">= 0.1.1"
|
18
|
+
gem.add_dependency "rack", ">= 1.0.1"
|
19
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 2.10.2"
|
20
|
+
gem.add_development_dependency "rack-test", ">= 0.5.2"
|
21
|
+
gem.add_development_dependency "notahat-machinist", ">= 1.0.3"
|
22
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
23
|
+
end
|
24
|
+
Jeweler::GemcutterTasks.new
|
25
|
+
rescue LoadError
|
26
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
27
|
+
end
|
28
|
+
|
29
|
+
require 'rake/testtask'
|
30
|
+
Rake::TestTask.new(:test) do |test|
|
31
|
+
test.libs << 'lib' << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
|
36
|
+
begin
|
37
|
+
require 'rcov/rcovtask'
|
38
|
+
Rcov::RcovTask.new do |test|
|
39
|
+
test.libs << 'test'
|
40
|
+
test.pattern = 'test/**/test_*.rb'
|
41
|
+
test.verbose = true
|
42
|
+
end
|
43
|
+
rescue LoadError
|
44
|
+
task :rcov do
|
45
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
task :test => :check_dependencies
|
50
|
+
|
51
|
+
task :default => :test
|
52
|
+
|
53
|
+
require 'rake/rdoctask'
|
54
|
+
Rake::RDocTask.new do |rdoc|
|
55
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
56
|
+
|
57
|
+
rdoc.rdoc_dir = 'rdoc'
|
58
|
+
rdoc.title = "octopus #{version}"
|
59
|
+
rdoc.rdoc_files.include('README*')
|
60
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
61
|
+
end
|
62
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/bin/octopus
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Octopus command line interface script.
|
4
|
+
# Run <tt>octopus -h</tt> to get more usage.
|
5
|
+
require File.dirname(__FILE__) + '/../lib/octopus'
|
6
|
+
require 'thin'
|
7
|
+
|
8
|
+
rackup_file = "#{File.dirname(__FILE__)}/../lib/octopus/config.ru"
|
9
|
+
|
10
|
+
argv = ARGV
|
11
|
+
argv << ["-R", rackup_file] unless ARGV.include?("-R")
|
12
|
+
argv << ["-p", "2001"] unless ARGV.include?("-p")
|
13
|
+
argv << ["-l", "/home/dave"] unless ARGV.include?("-l")
|
14
|
+
argv << ["-e", "production"] unless ARGV.include?("-e")
|
15
|
+
Thin::Runner.new(argv.flatten).run!
|
16
|
+
|
data/doc/octopus.zargo
ADDED
Binary file
|
data/lib/ext/partials.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# An implementation of partials for Sinatra.
|
2
|
+
#
|
3
|
+
# Can be used like: <%= partial(:foo) %>
|
4
|
+
#
|
5
|
+
# Unlike Rails partials, the .erb files for these partials do not start with a
|
6
|
+
# leading underscore, i.e. the file name should be foo.rb, not _foo.rb.
|
7
|
+
#
|
8
|
+
# Liberated from:
|
9
|
+
# http://github.com/cschneid/irclogger/blob/master/lib/partials.rb
|
10
|
+
#
|
11
|
+
module Sinatra
|
12
|
+
module Partials
|
13
|
+
def partial(template, *args)
|
14
|
+
options = args.extract_options!
|
15
|
+
options.merge!(:layout => false)
|
16
|
+
if collection = options.delete(:collection) then
|
17
|
+
collection.inject([]) do |buffer, member|
|
18
|
+
buffer << erb(template, options.merge(:layout =>
|
19
|
+
false, :locals => {template.to_sym => member}))
|
20
|
+
end.join("\n")
|
21
|
+
else
|
22
|
+
erb(template, options)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
data/lib/ext/reloader.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Reload scripts and reset routes on change
|
2
|
+
class Sinatra::Reloader < Rack::Reloader
|
3
|
+
def safe_load(file, mtime, stderr = $stderr)
|
4
|
+
if file == __FILE__
|
5
|
+
::Sinatra::Application.reset!
|
6
|
+
stderr.puts "#{self.class}: resetting routes"
|
7
|
+
end
|
8
|
+
super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
data/lib/init.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# Gems
|
2
|
+
#
|
3
|
+
require 'rubygems'
|
4
|
+
require 'sinatra'
|
5
|
+
require 'datamapper'
|
6
|
+
require 'ruby-debug'
|
7
|
+
require 'eventmachine'
|
8
|
+
require 'rack-flash'
|
9
|
+
require 'dm-validations'
|
10
|
+
|
11
|
+
|
12
|
+
# Extensions to Sinatra
|
13
|
+
#
|
14
|
+
require File.dirname(__FILE__) + '/ext/partials'
|
15
|
+
require File.dirname(__FILE__) + '/ext/array_ext'
|
16
|
+
require File.dirname(__FILE__) + '/ext/reloader'
|
17
|
+
|
18
|
+
# Octopus code
|
19
|
+
#
|
20
|
+
require File.dirname(__FILE__) + '/octopus'
|
21
|
+
require File.dirname(__FILE__) + '/octopus/models/subscription'
|
22
|
+
require File.dirname(__FILE__) + '/octopus/models/net_resource'
|
23
|
+
require File.dirname(__FILE__) + '/octopus/grabbers/generic_http'
|
24
|
+
|
25
|
+
configure :production do
|
26
|
+
db = "sqlite3:///#{Dir.pwd}/octopus.sqlite3"
|
27
|
+
DataMapper.setup(:default, db)
|
28
|
+
end
|
29
|
+
|
30
|
+
configure :development do
|
31
|
+
db = "sqlite3:///#{Dir.pwd}/octopus.sqlite3"
|
32
|
+
DataMapper.setup(:default, db)
|
33
|
+
end
|
34
|
+
|
35
|
+
configure :test do
|
36
|
+
db = "sqlite3::memory:"
|
37
|
+
DataMapper.setup(:default, db)
|
38
|
+
end
|
39
|
+
|
40
|
+
configure :production, :test, :development do
|
41
|
+
NetResource.auto_migrate! unless NetResource.storage_exists?
|
42
|
+
Subscription.auto_migrate! unless Subscription.storage_exists?
|
43
|
+
DataMapper.auto_upgrade!
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set the views to the proper path inside the gem
|
47
|
+
#
|
48
|
+
set :views, File.dirname(__FILE__) + '/octopus/views'
|
49
|
+
set :public, File.dirname(__FILE__) + '/octopus/public'
|
50
|
+
|
51
|
+
# Register helpers
|
52
|
+
#
|
53
|
+
helpers do
|
54
|
+
include Sinatra::Partials
|
55
|
+
alias_method :h, :escape_html
|
56
|
+
end
|
57
|
+
|
58
|
+
# Set up Rack authentication
|
59
|
+
#
|
60
|
+
use Rack::Auth::Basic do |username, password|
|
61
|
+
[username, password] == ['admin', 'admin']
|
62
|
+
end
|
63
|
+
|
64
|
+
# Include flash notices
|
65
|
+
#
|
66
|
+
use Rack::Session::Cookie
|
67
|
+
use Rack::Flash
|
68
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Grabbers
|
2
|
+
class GenericHttp
|
3
|
+
|
4
|
+
include EM::Protocols
|
5
|
+
|
6
|
+
# Adds a periodic timer to the Eventmachine reactor loop and immediately
|
7
|
+
# starts grabbing expired resources and checking them.
|
8
|
+
#
|
9
|
+
def initialize
|
10
|
+
@@currently_encoding = false
|
11
|
+
puts "Initializing generic http grabber..."
|
12
|
+
EM.add_periodic_timer(5) {
|
13
|
+
check_expired_resources
|
14
|
+
}
|
15
|
+
check_expired_resources
|
16
|
+
end
|
17
|
+
|
18
|
+
# Gets all of the expired NetResources from the database and sends an HTTP
|
19
|
+
# GET requests for each one. Subscribers to a NetResource will be notified
|
20
|
+
# if it has changed since the last time it was grabbed.
|
21
|
+
#
|
22
|
+
def check_expired_resources
|
23
|
+
net_resources = ::NetResource.expired
|
24
|
+
net_resources.each do |resource|
|
25
|
+
uri = URI.parse(resource.url)
|
26
|
+
conn = HttpClient2.connect uri.host, uri.port
|
27
|
+
|
28
|
+
req = conn.get(uri.path)
|
29
|
+
req.callback{ |response|
|
30
|
+
resource.set_next_update
|
31
|
+
if resource_changed?(resource, response)
|
32
|
+
notify_subscribers(resource)
|
33
|
+
update_changed_resource(resource, response)
|
34
|
+
end
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Notifies each of a NetResource's subscribers that the resource has changed
|
40
|
+
# by doing an HTTP GET request to the subscriber's callback url.
|
41
|
+
#
|
42
|
+
def notify_subscribers(resource)
|
43
|
+
resource.subscriptions.each do |subscription|
|
44
|
+
uri = URI.parse(subscription.url)
|
45
|
+
conn = HttpClient2.connect uri.host, uri.port
|
46
|
+
req = conn.get(uri.path)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Determines whether a resource has changed by comparing its saved hash
|
51
|
+
# value with the hash value of the response content.
|
52
|
+
#
|
53
|
+
def resource_changed?(resource, response)
|
54
|
+
changed = false
|
55
|
+
if response.content.hash != resource.last_modified_hash
|
56
|
+
changed = true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Updates the resource's fields when the resource has changed. The
|
61
|
+
# last_modified_hash is set to the hash value of the response body, the
|
62
|
+
# response body itself is saved so that it can be sent to consumers during
|
63
|
+
# notifications, and the resource's last_updated time is set to the current
|
64
|
+
# time.
|
65
|
+
#
|
66
|
+
def update_changed_resource(resource, response)
|
67
|
+
resource.last_modified_hash = response.content.hash
|
68
|
+
resource.last_updated = Time.now
|
69
|
+
resource.body = response.content
|
70
|
+
resource.save
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# Some kind of resource which we're watching for changes out on the interweb.
|
2
|
+
# Could be an RSS feed, a web page, etc.
|
3
|
+
#
|
4
|
+
class NetResource
|
5
|
+
include DataMapper::Resource
|
6
|
+
|
7
|
+
# Properties
|
8
|
+
#
|
9
|
+
property :id, Serial
|
10
|
+
property :url, String, :required => true, :length => (1..254)
|
11
|
+
property :last_modified_hash, Integer
|
12
|
+
property :update_period, Integer, :required => true, :default => 1200
|
13
|
+
property :next_update, DateTime, :required => true, :default => Time.now
|
14
|
+
property :last_updated, DateTime
|
15
|
+
property :body, Text
|
16
|
+
property :created_at, DateTime
|
17
|
+
property :updated_at, DateTime
|
18
|
+
|
19
|
+
# Associations
|
20
|
+
#
|
21
|
+
has n, :subscriptions
|
22
|
+
|
23
|
+
# Validations
|
24
|
+
#
|
25
|
+
validates_with_method :url, :method => :validate_url
|
26
|
+
validates_with_method :update_period, :method => :validate_update_period
|
27
|
+
|
28
|
+
# Checks that the url property is formatted correctly.
|
29
|
+
#
|
30
|
+
def validate_url
|
31
|
+
begin
|
32
|
+
uri = ::URI.parse(self.url)
|
33
|
+
if uri && uri.scheme == "http" || uri.scheme == "https"
|
34
|
+
return true
|
35
|
+
else
|
36
|
+
return [false, "Url must be properly formatted"]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
rescue ::URI::InvalidURIError
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns an array of all resources which have a next_update time which is
|
43
|
+
# less than or equal to Time.now.
|
44
|
+
#
|
45
|
+
def self.expired
|
46
|
+
all(:next_update.lte => Time.now)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Sets the next_update time equal to the current time plus the update_period
|
50
|
+
# for this resource.
|
51
|
+
#
|
52
|
+
def set_next_update
|
53
|
+
self.next_update = Time.now + update_period
|
54
|
+
self.save
|
55
|
+
end
|
56
|
+
|
57
|
+
# Ensures that the update_period is not less than 20 seconds.
|
58
|
+
#
|
59
|
+
def validate_update_period
|
60
|
+
unless self.update_period.nil? || self.update_period < 20
|
61
|
+
return true
|
62
|
+
else
|
63
|
+
[false, "Update period must be more than 20 seconds"]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# A url callback pointing to a web application or messaging system which wants
|
2
|
+
# to be notified about changes to a NetResource.
|
3
|
+
#
|
4
|
+
class Subscription
|
5
|
+
include DataMapper::Resource
|
6
|
+
|
7
|
+
# Properties
|
8
|
+
property :id, Serial
|
9
|
+
property :url, String, :required => true, :length => (1..254)
|
10
|
+
property :created_at, DateTime
|
11
|
+
property :updated_at, DateTime
|
12
|
+
|
13
|
+
# Associations
|
14
|
+
belongs_to :net_resource
|
15
|
+
|
16
|
+
# Validations
|
17
|
+
validates_with_method :validate_url
|
18
|
+
validates_present :net_resource
|
19
|
+
|
20
|
+
# Checks that the url property is formatted correctly.
|
21
|
+
#
|
22
|
+
def validate_url
|
23
|
+
begin
|
24
|
+
uri = ::URI.parse(self.url)
|
25
|
+
if uri && uri.scheme == "http" || uri.scheme == "https"
|
26
|
+
return true
|
27
|
+
else
|
28
|
+
return [false, "Url must be properly formatted"]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
rescue ::URI::InvalidURIError
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|