Sinatra 用 Ruby 快速开发一个静态服务全过程

vincenting · 2015年11月05日 · 最后由 lpgray 回复于 2015年12月17日 · 14473 次阅读
本帖已被管理员设置为精华贴

最新更新:添加最终演示的 sinatra 项目源码 https://github.com/vincenting/sinatra-image-server-example


由于各种原因,需要给快速客户端提供一个图片上传、生成缩略图、以及获取的接口。使用到的技术关键字:

  1. Sinatra - 基本的 web server 服务;
  2. Sidekiq - 提供异步任务执行支持;
  3. MiniMagick - 提供图片压缩切割支持;
  4. Nginx - 使用 rewrite 和 try_files 实现缩略图访问支持。

最终实现的目标:

  1. 支持客户端多图片上传;
  2. URL 中支持参数 resize,后台通过该参数生成对应的图片缩略图,并且可以通过 http://example.com/static/image.jpg@100x100 的方式来访问;
  3. 如果访问是缩略图未生产,则访问原图。

前方高能,代码块成堆!

Sinatra 简单的多图上传示例

post '/' do
  result = []
  begin
    params[:images].each do |f|
      filename = SecureRandom.hex(32) + File.extname(f[:filename])
      filepath = "#{ENV['STATIC_ROOT']}#{filename}"
      FileUtils.cp(f[:tempfile].path, filepath)
      result << "#{ENV['STATIC_URL']}#{filename}"
    end
  rescue Exception => e
    halt 400
  end
  json msg: 'upload success.', urls: result
end

然后使用 minitest 来进行测试,或者写一个简单的页面来测试。

<html>
  <head><title>Multi file upload</title></head>
  <body>
    <form action="/" method="post" enctype="multipart/form-data">
      <input type="file" name="images[]" multiple />
      <input type="submit" />
    </form>
  </body>
</html>

使用 Sidekiq 和 MiniMagick 完成缩略图制作

首先需要实现一个简单的缩略图 Worker

class ImageResizeWorker
  include Sidekiq::Worker

  def perform(image_path, sizes)
    image_info = {}
    [:dirname, :extname].each do |f|
      image_info[f] = File.send f, image_path
    end
    image_info[:basename] = File.basename image_path, image_info[:extname]
    sizes.each do |size|
      output_path = File.join(image_info[:dirname],
                              "#{image_info[:basename]}@#{size}#{image_info[:extname]}")
      resize_image image_path, size, output_path
    end
  end

  private
  def resize_image(image_path, output_size, output_path)
    image = MiniMagick::Image.open image_path
    image.resize output_size
    image.write output_path
  end

end

使用测试用例测试 worker,测试用例基本如下:

require_relative 'spec_helper'

describe ImageResizeWorker do
  before do
    @test_image_path = File.join(File.dirname(__FILE__), 'test_image_files', 'wensen.jpg')
    @worker = ImageResizeWorker.new
  end

  it 'should resize images without any error' do
    @worker.perform(@test_image_path, %w(100x100 200x200))
  end

  it 'should has the same extname' do
    @worker.perform(@test_image_path, %w(100x100 200x200))
    assert File.exist?(File.join(File.dirname(@test_image_path), "[email protected]"))
    assert File.exist?(File.join(File.dirname(@test_image_path), "[email protected]"))
  end

  it 'should assert_equal name and size' do

    @worker.perform(@test_image_path, %w(40x50 100x80 120x120))
    Dir[File.join(File.dirname(__FILE__), 'test_image_files', '*@*.*')].each do |f|
      image = MiniMagick::Image.open(f)
      size = File.basename(f, '.*').split('@').last.split 'x'
      assert_equal image.width, size.first.to_i
      assert_equal image.height, size.last.to_i
    end
  end

  after do
    Dir[File.join(File.dirname(__FILE__), 'test_image_files', '*@*.*')].each { |f| File.delete f }
  end
end

当测试到大小比例和原图不等的时候突然发现报错了,发现 resize 没有自动切割(或许是有参数我不知道)。于是就要继续拓展 resize_image 方法来支持切割。此处略去一大块代码。

修改 webserver 的代码,在图片拷贝到上传文件夹后,

unless ENV['RACK_ENV'] == 'test'
  ImageResizeWorker.perform_async filepath, (params[:resize] || '').split(',')
end

然后就是测试测试,这个时候基本上一个支持多图片上传,并且支持后台图片缩略图的 server 就完成了。

Nginx 优化图片访问方式

在下面的讨论内容之前,先贴一个地址 IF IS EVAL

需求:访问 /static/url.jpg@100x100 可以访问到图片 /opt/static/[email protected] 并返回,如果这个时候缩略图还没生成,就先返回原图。

先不管第二个需求,写出来的基本上就是如下的配置:

server {
    root /opt;
    location /static/ {
       rewrite  ^/static/(\w+)\.(\w+)@(\w+)  /static/$1@$3.$2 break;
    }
}

默认完成图片的访问地址与硬盘地址对应,如果满足 rewrite 正则则执行 rewrite,即需求中的第一点。现在需要完成第二条需求,即如果默认的访问失败的话,就去访问原图,于是这里需要使用到 try_files - 按照特定顺序来尝试 file 直到第一个成功。

server {
    root /opt;
    location /static/ {
       try_files $uri $uri.origin;
       rewrite  ^/static/(\w+)\.(\w+)@(\w+)  /static/$1@$3.$2 break;
    }
    location ~ \.origin$ {
    rewrite  ^/static/(\w+)\@(\w+).(\w+).origin$  /static/$1.$3 break;
    }
}

于是最终就变成了如上的配置文件(虽然一路并不顺利)。在同时使用 try_files 和 rewrite 的时候,发现执行顺序是先 尝试第一个,即 $uri,然后执行 /static/ 中的 rewrite,此时的 $uri 其实已经变成了 rewrite 后的内容,所以当 /static/ 没有找到文件换为 $uri.origin 这里的 $uri 已经不是原来的请求路径,而是 rewrite 后的。所以对于特别是新人,在写 nginx 配置的时候要记得看 error 输出,如果没有的话记得开启,里面会给出很多有价值的内容。


吐槽完毕,最终在 sinatra 中通过 before 的方法加上基本的鉴权项目就上了测试环境。

最快的难道不是贩卖一个 云 服务账号给他们,赚差价?

#1 楼 @huobazi 客户端 =- =。。不是客户。。

文笔思路很清晰,学习了

如果图片不大的话,nginx 有原生的库直接支持图片的 resize....

#4 楼 @killyfreedom 顺着这个思路搜到了一个很赞的配置 https://gist.github.com/tmaiaroto/9450785

不过如果不是所有人都可以上传(权限)的话,可能就需要 nginx lua 模块来在 image_filter 前进行权限控制了(或者用 try_files 来一些很极客的方法)。

#5 楼 @vincenting 我们线上一直在用这个,但是这个图片不能太大,图片太大转换会出错

顶二老板。

如果是我来完成这个需求 我更倾向于基于 OSS 来开发,很大一个原因是 OSS 可以提供图片缩放服务 一方面,有很大一部分逻辑可以砍掉,减少系统复杂度,降低维护成本 另一方面,OSS 的图像处理是实时 + 缓存,实际上长期保存的图片只有原图一张而已,如果以后系统有变迁需要改变图片大小,不用头大 然后很关键的一点,OSS 还自带 CDN

这样的需求我一般是用 carrierwave+carrierwave-aliyun+carrierwave_backgrounder+sidekiq 解决的,不要重复造轮子嘛

#10 楼 @zhangtenghai carrierwave+carrierwave-aliyun 我也是用的这两货

#9 楼 @xworm 我也会选择用 OSS,自己搭建成本太高了,虽然前期快速搭建起来了,后期维护的工作更重,感觉一般大厂才自己搭建

#12 楼 @psvr OSS 有 OSS 的好处,自建有自建的好处。 自建最大的好处是:所有的东西都在自己的掌控中。 而 OSS 并不一定满足您的所有需求,即使满足,也不一定贴合您的心意。

#12 楼 @psvr 后期维护工作重 => 如果需要后期量到一定程度,直接数据迁移到专门的静态云就可以了,同时还可以采取在后台同步到第三方静态服务器的方案(毕竟对于客户端/前端只是一个 url 的问题) 。前期客户端前端对接快目的就达到了就可以了。 #11 楼 @xworm 你还不知道我是造轮子专业户么 😉

+1,Nginx 优化图片访问方式 这一节很有启发,学到了

没看懂,先标记一下。

相当不错呢!

文哥雄起

需要 登录 后方可回复, 如果你还没有账号请 注册新账号