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