追踪图片在外部系统的查看次数

2010-05-20 22:12:48 +0800

随着sns网站和web api的兴起,与第三方网站的交互越来越多。比如我们可以向用户facebook好友上的wall推送数据等等来做网站的推广,这就引来一个问题,我们不能只傻乎乎地做推广,更重要的是统计推广的效果。你总共推送了多少数据,有多少人看到了这些数据,又有多少人点击来到了你的网站?这些数据都是可以用来帮助你改进和提高推广的效果。

对于推送了多少数据和有多少人看到了这些数据,这两者比较容易做到,前者可以在推送数据的时候做记录,后者可以在用户点击进入网站的时候做记录。对于有多少人看到了这些数据就比较麻烦了。

下面拿facebook为例介绍如何统计数据在外部系统的查看次数。如果是纯文字的话几乎没法做到,但是如果是推送图片或视频的话,可以通过图片的显示次数来统计数据。

一般往facebook推送图片或视频等图片的时候,只需要在推送的参数中设置图片或视频的完整url即可。然后facebook在显示图片或视频的时候,会发送请求到服务器上,你只需要捕获这个请求并添加相应的逻辑处理。但是问题是,一般部署的rails应用,除了使用rails server(如thin),还会放置一个web server(如nginx)来做负载均衡,你的图片或视频的请求会直接被web server处理,根本不经过rails server,这样你的逻辑代码就永远不会被调用到了。

解决的方法就是把传递给facebook的图片或视频url由静态url改成动态url,比如:http://yourdomain.com/assets/test.png改成http://yourdomain.com/assets/1,然后由assets_controller来返回图片

class AssetsController < ApplicationController
  def show
    asset = Asset.find(params[:id])
    # add logic to increment asset show count
   
    send_file asset.attachment.url(:small, false)
  end
end

到此为止,你已经可以统计你推送的数据被多少人看到了,不过事情到这里还没有结束,通过rails server来上传图片效率是很低的,你应该将这项任务交给web server。以nginx服务器为例,它提供了X-Accel-Redirect选项,负责静态文件的上传。

首先,修改nginx的配置文件,将/assets路径加入到X-Accel-Redirect

location /assets {
  root /var/www/staging/current/public;
  internal;
}

这段配置的作用是,如果X-Accel-Redirect指定的路径为/assets/image.png,那么nginx会去寻找/var/www/staging/current/public/assets/image.png文件并上传

然后要将推送到facebook的图片或视频路径由assets/1改为facebook_assets/1,避免和X-Accel-Redirect冲突。

class FacebookAssetsController < ApplicationController
  def show
    asset = Asset.find(params[:id])
    # add logic to increment asset show count
   
    response.headers['X-Accel-Redirect'] = asset.attachment.url(:small, false)
    render :nothing => true
  end
end

 可以看到,只需要在reponse header中增加X-Accel-Redirect即可,render :nothing => true表示rails server不处理图片的上传。nginx看到response header中有X-Accel-Redirect选项,就到/var/www/staging/current/public目录下面去寻找相应的图片并上传。

Lighttpd和Apache2则可以通过X-Sendfile选项来完成同样的事情。

Ruby1.9的中文问题

2010-05-15 21:59:53 +0800

今天在ruby 1.9.1的环境下试了一下1.8.7下面写的一段代码,结果报错:syntax error, unexpected $end, expecting '}',查看了一下代码,如下

STATUS = { 
  "400" => "在线", 
  "300" => "离开", 
  "600" => "繁忙", 
  "0" => "脱机" 
}

语法完全没有问题,判断是中文导致的问题,奇怪的是在1.8.7下面运行正常。google了一下,原来只要在文件开头加上coding就可以了

#coding: utf-8

 

为resque写扩展

2010-05-13 23:43:31 +0800

resque是基于redis的ruby类库,用于创建后台任务,把这些后台任务放在多个队列中去,之后在处理它们。github就是使用resque来处理它们的后台任务的。

对于需要长时间处理的任务,比如发送email,发tweet,图片resize等等,都是resque的用武之地。默认resque就是将任务加到redis的队列中去,然后定时取出来去处理,实际项目中我们往往需要对其增加额外的扩展,比如你需要增加日志功能,增加处理次数的限制,这个时候就可以给resque写一个plugin,就像rails的plugin一样。

resque定义了非常良好的HOOK,使得为其写扩展变得更加容易。

resque采取的是每隔n秒从队列中获取一个任务,然后fork一个子进程来执行这个任务。resque定义了before_fork, after_fork, before_perform, after_perform, around_perform, on_failure几个hook,执行顺序如下

before_fork

fork

after_fork

before_perform

around_perform

perform

around_perfomr

after_perform

还有就是发生错误的时候,on_failure会被执行。

再给一个我写的resque-restriction插件的实例

def before_perform_restriction(*args)
  settings.each do |period, number|
    key = redis_key(period)
    value = get_restrict(key)

    if value.nil? or value == ""
      set_restrict(key, seconds(period), number)
    elsif value.to_i <= 0
      Resque.push "restriction", :class => to_s, :args => args
      raise Resque::Job::DontPerform
    end
  end
end

def after_perform_restriction(*args)
  settings.each do |period, number|
    key = redis_key(period)
    Resque.redis.decrby(key, 1)
  end
end

before_perform_restriction检查任务在一个时间段执行的次数,如果执行次数超过规定,抛出Resque::Job::DontPerform异常,它将终止该任务继续执行。

after_perform_restriction则在任务执行之后,将计数器减一。

可以看出,resque作为一个后台任务的框架,其api设计非常良好,很容易对其进行扩展,应该多学习学习。

git for hostmonster

2010-05-02 14:17:41 +0800

前段时间对网站做了些更新,于是在本地修改了代码,再git push,谁知却得到“bash: git-receive-pack: command not found”的error,我的git repository是放在hostmonster服务器上面的,之前都是正常的,于是提交ticket给hostmonster的support,得到的答复是他们升级的openssh,通过git+ssh不会再读取.bashrc或.ssh/environment文件,也就是说通过git+ssh没有办法修改PATH了。

没办法了,只能手动将命令的路径补全了,对于git pull/git ps来说,只需要在输入命令的时候增加参数,比如

git clone --upload-pack=/home1/huangzhi/git/bin/git-upload-pack
git push --receive-pack=/home1/huangzhi/git/bin/git-receive-pack

不过每次都输入参数实在麻烦,直接写到配置文件.git/config

[remote "origin"]
uploadpack=/home1/huangzhi/git/bin/git-upload-pack
receivepack=/home1/huangzhi/git/bin/git-receive-pack

然后就可以像以前一样git pull/git ps了。

还有一个问题,那就是capistrano。默认capistrano通过git  ls-remote获取最新的commit id,通过git clone来获取最新文件,但是这些命令都没有办法设置upload-pack和receive-pack参数,没办法,只能修改默认的方法定义。

首先是git ls-remote

require 'capistrano/recipes/deploy/scm/base'
::Capistrano::Deploy::SCM::Base.class_eval do
  alias_method :origin_scm, :scm
  def scm(*args)
    if command == "git" and args[0] == "ls-remote"
      args[0] = "ls-remote --upload-pack=/home1/huangzhi/git/bin/git-upload-pack"
    end
    origin_scm(args)
  end
end

当命令为git ls-remote的时候,额外加入参数upload-pack

再就是git checkout

require 'capistrano/recipes/deploy/scm/git'
::Capistrano::Deploy::SCM::Git.class_eval do
  def checkout(revision, destination)
    git    = "/home1/huangzhi/git/bin/git"
    remote = origin

    args = []
    args << "-o #{remote}" unless remote == 'origin'
    if depth = configuration[:git_shallow_clone]
      args << "--depth #{depth}"
    end

    execute = []
    if args.empty?
      execute << "#{git} clone --upload-pack=/home1/huangzhi/git/bin/git-upload-pack #{verbose} #{configuration[:repository]} #{destination}"
    else
      execute << "#{git} clone --upload-pack=/home1/huangzhi/git/bin/git-upload-pack #{verbose} #{args.join(' ')} #{configuration[:repository]} #{destination}"
    end

    # checkout into a local branch rather than a detached HEAD
    execute << "cd #{destination} && #{git} checkout #{verbose} -b deploy #{revision}"
    
    if configuration[:git_enable_submodules]
      execute << "#{git} submodule #{verbose} init"
      execute << "#{git} submodule #{verbose} sync"
      execute << "#{git} submodule #{verbose} update"
    end

    execute.join(" && ")
  end
end

这个我没有找个比较优雅的方式,只能直接覆盖原来的方法定义,并在git clone的命令中加入upload-pack。

还有就是当capistrano执行远程命令的时候,同样没有合适的environments,比如执行rake db:migrate的时候,所以需要修改默认的rake命令

set :rake, "source /home1/huangzhi/.bashrc; rake"

这样当执行rake命令执行,首先读取.bashrc,设置合适的environments,然后再执行rake命令。

到此为止,一切又恢复了正常。

select和input type=file两个都是html标签,但是它们在不同的浏览器上显示是完全不同,对于那些对UI要求非常高的网站来说,这是不可接受的。由于这两个标 签的样式是由浏览器实现的,所以要想完全通过css来统一样式几乎是不可能的,所以我们这里需要借助javascript的帮助。

比如我们 想要把input type=file做成下面这个样子

看上去这个应该由两个标签组成,左边是一个text field,右边是一个上传的按钮,要想在所有的浏览器上都把input type=file做成这个样子好像没这个可能,可以想到的办法就是设置两个层,下面的层由一个text field和一个按钮组成,上面是一个透明的input type=file的层,高度和宽度正好覆盖下面的层就可以了。

我是借助 javascript来生成下面的那个层

$.each($('form input[type=file]'), function(i, elem) {
  $(elem).parent().append($("<div class='fakefile'><input type='text'><div class='browser_button'></div></div>"));
  $(elem).change(function() {
    $(this).parent().find('input[type=text]').val($(this).val());
  });
});

这里fakefile就是下面的那个层,<div class="browser_button"></div>是通过css_sprite得到的按钮图片(最近使用 css_sprite到了偏执的状态)。然后就是通过css来区分上下两个层

.file {
  width: 209px;
  height: 28px;
  position: relative; }
.file input[type=file] {
  z-index: 2;
  width: 278px;
  opacity: 0;
  filter: alpha(opacity: 0);
  -moz-opacity: 0;
  cursor: pointer; }
.file div.fakefile {
  z-index: 1;
  position: absolute;
  top: 0;
  left: 0; }
.file div.fakefile input {
  width: 207px;
  height: 28px;
  border: 1px solid #c1c1c1;
  -moz-border-radius: 3px;
  -webkit-border-radius: 3px; }
.file div.fakefile .browser_button {
  position: absolute;
  top: 0;
  left: 207px; }

input type=file的z-index为2,.fakefile的z-index为1,表示fakefile为下面的层,input type=file为上面的层。input type=file的opacity设置为0,表示input type=file为透明的,通过position: absolute可以将两个层完全重叠。这样做出来的input type=file就和之前的图片一模一样了。

对于select标签的样式修改也可以用相同的办法进行处理。