Ruby & mDNS: Finding Information About Your Network
I've been working on a little project that was scanning information on my network and I was trying to figure out how I can get human friendly names to go with IP addresses. Not surprisingly there is a gem that does most of the work, DNSSD.
Installation
gem install dnssd
Discover Services
The process is two fold. First you need to get all the neighbours who are broadcasting on a certain service. I've used _afpovertcp as most OS X boxes broadcast AFP, but there are loads you can listen for. Check out the list at: DNS SRV (RFC 2782).
hosts = {} begin # Only listen for one second timeout 1 do browser.browse('_afpovertcp._tcp', 'local') do |reply| hosts[reply.fullname] = reply next if reply.flags.more_coming? hosts.sort_by do |_, host| [(host.flags.add? ? 0 : 1), host.fullname] end.each do |_, host| next unless host.flags.add? # Do something with each of the services we've found. end end end rescue Timeout::Error ; end
Find IPs for Hosts
Next we want to look up the IPs for a specific mDNS client. As each remote host can have several names for an address we will need to add all the addresses for the remote host. We can use the resolve to find the addresses:
ip_addresses = [] # Assume reply is one of the hosts we've enumerated DNSSD.resolve!(reply) do |r| service = DNSSD::Service.new begin # Use the server object to get address info about the target service.getaddrinfo(r.target, 0) do |addrinfo| ip_addresses << addrinfo.address break unless addrinfo.flags.more_coming? end ensure service.stop end end
All Together Now
require 'rubygems' require 'dnssd' hosts = {} hostnames = {} begin browser = DNSSD::Service.new timeout 1 do browser.browse('_afpovertcp._tcp', 'local') do |reply| hosts[reply.fullname] = reply next if reply.flags.more_coming? hosts.sort_by do |_, host| [(host.flags.add? ? 0 : 1), host.fullname] end.each do |_, host| next unless host.flags.add? ip_addresses = [] DNSSD.resolve!(host) do |r| service = DNSSD::Service.new begin service.getaddrinfo(r.target, 0) do |addrinfo| ip_addresses << addrinfo.address break unless addrinfo.flags.more_coming? end ensure service.stop end break end ip_addresses.each { |ip| hostnames[ip] = host.name } end end end rescue Timeout::Error ; end # hostnames will now contain a hash of ip_address => hostname pairs
Using Paperclip with Datamapper & Sinatra
I love using Sinatra, for small projects it's so fast to get things up and running. I had a small project for a file hosting service that I wanted to get running on Sinatra with Paperclip. Although Paperclip was originally built for rails Ken Robertson of In Valid Logic ported it to Datamapper.
Setup The Model
This starts to make things really easy. Simply declare you model with some extensions and you're almost set:
class Resource include DataMapper::Resource include Paperclip::Resource property :id, Serial has_attached_file :file, :url => "/system/:attachment/:id/:style/:basename.:extension", :path => "#{APP_ROOT}/public/system/:attachment/:id/:style/:basename.:extension" end
You'll need to specify your :url and :path options as the ones built into dm-paperclip are merb centric which won't quite work. Also set APP_ROOT to where ever you're application root directory with you static Sintra folder is.
Setup Your Route
In your routes you'll have something like:
post '/upload' do resource = Resource.new(:file => make_paperclip_mash(params[:file])) halt "Something went wrong..." unless resource.save end
The tricky part here is the make_paperclip_mash method. Paperclip expects the file object loaded from the form to be in a different form than what is created by default. To fix this We create a Mash (which is just a Hash, unless you're actually using merb) with some specific attributes:
def make_paperclip_mash(file_hash) mash = Mash.new mash['tempfile'] = file_hash[:tempfile] mash['filename'] = file_hash[:filename] mash['content_type'] = file_hash[:type] mash['size'] = file_hash[:tempfile].size mash end
That should pretty much get everything up and running.
Putting it all Together
To get the demo working you'll need a bunch of gems:
sudo gem install datamapper sinatra dm-paperclip haml
And the code:
require 'rubygems' require 'sinatra' require 'datamapper' require 'dm-paperclip' require 'haml' require 'fileutils' APP_ROOT = File.expand_path(File.dirname(__FILE__)) DataMapper::setup(:default, "sqlite3://#{APP_ROOT}/db.sqlite3") class Resource include DataMapper::Resource include Paperclip::Resource property :id, Serial has_attached_file :file, :url => "/:attachment/:id/:style/:basename.:extension", :path => "#{APP_ROOT}/public/:attachment/:id/:style/:basename.:extension" end Resource.auto_migrate! def make_paperclip_mash(file_hash) mash = Mash.new mash['tempfile'] = file_hash[:tempfile] mash['filename'] = file_hash[:filename] mash['content_type'] = file_hash[:type] mash['size'] = file_hash[:tempfile].size mash end post '/upload' do halt 409, "File doesn't appeart to have any content." unless params[:file][:tempfile].size > 0 @resource = Resource.new(:file => make_paperclip_mash(params[:file])) halt 409, "Something went wrong...\n#{resource.errors.inspect}" unless @resource.save haml :upload end get '/' do haml :index end __END__ @@ index %form{:action => '/upload', :enctype => 'multipart/form-data', :method => 'POST'} %input{:type => 'file', :name => 'file'} %input{:type => 'submit'} @@ upload %p= "Shiny, your file #{@resource.file.name} uploaded!" %img{:src => @resource.file.url }
Let Flash Die (YouTube Supports HTML5!)
Flash has been a giant on the Internet for some time. It's done some a lot of good and a lot of bad. Either way it's a gross mess, most things can't index it, it's piggishly CPU heavy much of the time, and many sites that use it are usability nightmares. HTML5 and stylesheets are very powerful, throw a bit of Javascript on them and you can have beautiful sites, you don't need Flash people.
It's time for it to start dying, and Google, doing no evil, is on the side of good. They've added beta HTML5 support. It only works in Chrome and Safari right now. Firefox doesn't support the video format they stream (h264, Firefox support Ogg Theora), so I'd imagine they'd have to re-encode which would be huge amount of data to process.
There's also a handy Safari addon called ClickToFlash which disables flash in websites until you click the frame where it'd be. This is great for getting rid of countless advertisements and websites that like to talk to you. You can also convert your videos to HTML5 videos on YouTube with ClickToFlash, which really ins't necessary with Google's beta.
For Firefox you've got Flashblock which has similar features to ClickToFlash without the YouTube support (which isn't really necessary now Google has added the beta opt in).
Subclassing NSManagedObject
So I looked high an low and it took me a while to find out some of the best practices when creating a subclass of NSManagedObject. For the most part you don't even need to all the properties will be there without any coding needed, but frequently you need to subclass it, to create logic and the like.
In my case I needed to be able to enumerate certain properties so I wanted to know the best way to subclass it and declare the properties on the model. I guess this is here for my own interest.
So you have a model called country with id, name, and population:
Country.h:
@interface Country : NSManagedObject { } @property (nonatomic, retain) NSNumber *id; @property (nonatomic, retain) NSString *name; @property (nonatomic, retain) NSNumber *population; @end
Country.m:
@implementation Country @dynamic id; @dynamic name; @dynamic population; @end
Now methods like class_copyPropertyList will return the attributes specified. Sweet!
Having Authlogic Check Login & EMail on Authentication
This is kinda documented in the Authlogic docs, but it's not super clear, so I figured I'd try to clear things up.
Problem
I have an email field that isn't always populated and I have a login field that isn't always populated (they are unique). How do I get the authentication to check both columns?
Using find_by_login_method
Authlogic lets you override just about anything, you just need to know the where and the how.
First off you'll need to create you custom model finder in your User model:
class User < ActiveRecord::Base def self.find_by_username_or_email(login) find_by_username(login) || find_by_email(login) end end
Next you need to tell Authlogic to use that instead of the build in one. This was the unclear part. You put that in your UserSession object:
class UserSession < Authlogic::Session::Base find_by_login_method :find_by_login_or_email end
Anyway I hope that helps somebody.
Battle with Conflicting Ruby & MacRuby Gems
I've been playing with macruby in my down time a bit lately, and it's wicked cool, but there are a couple things to be careful of. One is how gems work.
First off all the macruby commands are prefixed with mac so they don't collide with the regular commands. This is important seeing as how the runtime is a fair bit different on this inside.
This works very well except when the default path for installing binaries for gems is the same on all ruby version. Namely /usr/bin/ so when you install a gem as root it will blow out your built in on and then start making calls to the macruby binaries.
Then you start getting stuff like this:
Rails requires RubyGems >= 1.3.2 (you have 1.3.1). Please `gem update --system` and try again.
When gem -v reports 1.3.5 and confusion sets in (and hours of frustration).
So the trick is, get macgems to install just to the local .gem folder. Or go and make sure that gems are killing each other's binaries. Then you just need to make sure your paths are all happy.
HighLine
Asking for input in a Rake Task
It's pretty easy asking for input in a rake task, but no what you'd expect.
This doesn't work:
puts "Please enter a username:" username = gets.chomp
You need to actually specify STDIN:
puts "Please enter a username:" username = STDIN.gets.chomp
This is all fine an dandy for non-secure data, but what about passwords, it'd be nice to have them not show up. Well there's a gem for that.
A better way: HighLine
HighLine makes command-line interaction very simple:
Password Input:
password = HighLine.new.ask("Please enter the 'admin' password: ") do |q| q.echo = false end
Validation:
username = HighLine.new.ask("Please enter the 'username': ") do |q| q.validate = /[a-zA-z][a-zA-Z0-9]{2,8}$/ end
It does a whole lot of other stuff too, so check out the docs.
Basic Rails Usage:
environment.rb:
gem.config 'highline', :lib => false, :version => '>=1.5.1'
rake.task:
require 'highline' namespace :generate do desc "Do something with CLI" task "Enter username & password" do username = HighLine.new.ask("Please enter the 'username': ") do |q| q.validate = /[a-zA-z][a-zA-Z0-9]{2,8}$/ end password = HighLine.new.ask("Please enter the 'admin' password: ") do |q| q.echo = false end end end
Javascript Getters & Setters
K well this doesn't work in IE--shocking I know--however, it's pretty cool (the ActionScript folk will like it). You can have getter and setter properties in Javascript now. No need to have a winter.setSnow("Go Away") and winter.getSnow() style functions.
It's easy as pie:
Winter = function() { var privateStyleWinterStatus = "All the time"; return { get snow: function() { return privateStyleWinterStatus; }, set snow: function(val) { privateStyleWinterStatus = val; } }; }; // And now we can go like: var winter = new Winter; winter.snow = 'Go Away!'; console.log(winter.snow);
No I can hear the questions: "That's all fine and dandy, but what if already have an object?". Well that's a bit more cryptic, but good all the same (and you don't get stuck with the rigidity of ActionScript on this one):
var weather = function() { return ["blizzard", "clear", "light skiff", "freezing rain"][Math.round(Math.random() * 4)] }; winter.__defineGetter__("weather", weather); console.log(winter.weather); console.log(winter.weather); console.log(winter.weather); console.log(winter.weather);
Formtastic for an Authlogic Session
With the latest version of Formtastic (0.9.4) I started getting errors going to my login page:
ActionView::TemplateError (undefined method `content_columns' for UserSession:Class)
It seems that the latest version uses the content_columns method to get information about the model. This confuses our Authlogic Authlogic::Session::Base model as it's not really an ActiveRecord and it doesn't have this method.
Solution: Don't use the form.inputs method
Change this:
- semantic_form_for @user_session do |form| = form.inputs = form.buttons
To This:
- semantic_form_for @user_session do |form| - form.inputs do = form.input :login = form.input :password = form.buttons