Merge pull request #59 from 5apps/swift

Add Swift provider
This commit is contained in:
Greg Karékinian 2015-04-29 20:28:30 +02:00
commit c29e681a40
8 changed files with 1006 additions and 684 deletions

View File

@ -5,6 +5,8 @@ gem "sinatra-contrib"
gem "activesupport", '~> 3.2' gem "activesupport", '~> 3.2'
gem "riak-client", :github => "5apps/riak-ruby-client", :branch => "invalid_uri_error" gem "riak-client", :github => "5apps/riak-ruby-client", :branch => "invalid_uri_error"
gem "fog" gem "fog"
gem "rest-client"
gem "redis"
group :test do group :test do
gem 'rake' gem 'rake'
@ -14,4 +16,5 @@ end
group :staging, :production do group :staging, :production do
gem "rainbows" gem "rainbows"
gem "sentry-raven"
end end

View File

@ -21,6 +21,8 @@ GEM
builder (3.2.2) builder (3.2.2)
eventmachine (1.0.3) eventmachine (1.0.3)
excon (0.16.10) excon (0.16.10)
faraday (0.9.1)
multipart-post (>= 1.2, < 3)
fog (1.7.0) fog (1.7.0)
builder builder
excon (~> 0.14) excon (~> 0.14)
@ -42,6 +44,7 @@ GEM
mime-types (1.23) mime-types (1.23)
minitest (2.10.0) minitest (2.10.0)
multi_json (1.10.0) multi_json (1.10.0)
multipart-post (2.0.0)
net-scp (1.0.4) net-scp (1.0.4)
net-ssh (>= 1.99.1) net-ssh (>= 1.99.1)
net-ssh (2.6.7) net-ssh (2.6.7)
@ -59,7 +62,12 @@ GEM
unicorn (~> 4.8) unicorn (~> 4.8)
raindrops (0.13.0) raindrops (0.13.0)
rake (0.9.2.2) rake (0.9.2.2)
redis (3.0.7)
rest-client (1.6.7)
mime-types (>= 1.16)
ruby-hmac (0.4.0) ruby-hmac (0.4.0)
sentry-raven (0.13.1)
faraday (>= 0.7.6)
sinatra (1.4.5) sinatra (1.4.5)
rack (~> 1.4) rack (~> 1.4)
rack-protection (~> 1.4) rack-protection (~> 1.4)
@ -87,6 +95,9 @@ DEPENDENCIES
purdytest purdytest
rainbows rainbows
rake rake
redis
rest-client
riak-client! riak-client!
sentry-raven
sinatra (~> 1.4) sinatra (~> 1.4)
sinatra-contrib sinatra-contrib

View File

@ -13,6 +13,13 @@ development: &defaults
cs_binaries: rs.binaries cs_binaries: rs.binaries
authorizations: rs_authorizations authorizations: rs_authorizations
opslog: rs_opslog opslog: rs_opslog
# # uncomment this section and comment the riak one
# swift: &swift_defaults
# host: "https://swift.example.com"
# # Redis is needed for the swift backend
# redis:
# host: localhost
# port: 6379
test: test:
<<: *defaults <<: *defaults

View File

@ -15,7 +15,7 @@ module RemoteStorage
self.settings = settings self.settings = settings
self.server = server self.server = server
credentials = File.read(settings['riak_cs']['credentials_file']) credentials = File.read(settings.riak['riak_cs']['credentials_file'])
self.cs_credentials = JSON.parse(credentials) self.cs_credentials = JSON.parse(credentials)
end end
@ -229,8 +229,7 @@ module RemoteStorage
# A URI object that can be used with HTTP backend methods # A URI object that can be used with HTTP backend methods
def riak_uri(bucket, key) def riak_uri(bucket, key)
rc = settings.symbolize_keys URI.parse "http://#{settings.riak["host"]}:#{settings.riak["http_port"]}/riak/#{bucket}/#{key}"
URI.parse "http://#{rc[:host]}:#{rc[:http_port]}/riak/#{bucket}/#{key}"
end end
def serializer_for(content_type) def serializer_for(content_type)
@ -471,13 +470,13 @@ module RemoteStorage
end end
def client def client
@client ||= ::Riak::Client.new(:host => settings['host'], @client ||= ::Riak::Client.new(:host => settings.riak['host'],
:http_port => settings['http_port']) :http_port => settings.riak['http_port'])
end end
def data_bucket def data_bucket
@data_bucket ||= begin @data_bucket ||= begin
bucket = client.bucket(settings['buckets']['data']) bucket = client.bucket(settings.riak['buckets']['data'])
bucket.allow_mult = false bucket.allow_mult = false
bucket bucket
end end
@ -485,7 +484,7 @@ module RemoteStorage
def directory_bucket def directory_bucket
@directory_bucket ||= begin @directory_bucket ||= begin
bucket = client.bucket(settings['buckets']['directories']) bucket = client.bucket(settings.riak['buckets']['directories'])
bucket.allow_mult = false bucket.allow_mult = false
bucket bucket
end end
@ -493,7 +492,7 @@ module RemoteStorage
def auth_bucket def auth_bucket
@auth_bucket ||= begin @auth_bucket ||= begin
bucket = client.bucket(settings['buckets']['authorizations']) bucket = client.bucket(settings.riak['buckets']['authorizations'])
bucket.allow_mult = false bucket.allow_mult = false
bucket bucket
end end
@ -501,7 +500,7 @@ module RemoteStorage
def binary_bucket def binary_bucket
@binary_bucket ||= begin @binary_bucket ||= begin
bucket = client.bucket(settings['buckets']['binaries']) bucket = client.bucket(settings.riak['buckets']['binaries'])
bucket.allow_mult = false bucket.allow_mult = false
bucket bucket
end end
@ -509,7 +508,7 @@ module RemoteStorage
def opslog_bucket def opslog_bucket
@opslog_bucket ||= begin @opslog_bucket ||= begin
bucket = client.bucket(settings['buckets']['opslog']) bucket = client.bucket(settings.riak['buckets']['opslog'])
bucket.allow_mult = false bucket.allow_mult = false
bucket bucket
end end
@ -520,12 +519,12 @@ module RemoteStorage
:provider => 'AWS', :provider => 'AWS',
:aws_access_key_id => cs_credentials['key_id'], :aws_access_key_id => cs_credentials['key_id'],
:aws_secret_access_key => cs_credentials['key_secret'], :aws_secret_access_key => cs_credentials['key_secret'],
:endpoint => settings['riak_cs']['endpoint'] :endpoint => settings.riak['riak_cs']['endpoint']
}) })
end end
def cs_binary_bucket def cs_binary_bucket
@cs_binary_bucket ||= cs_client.directories.create(:key => settings['buckets']['cs_binaries']) @cs_binary_bucket ||= cs_client.directories.create(:key => settings.riak['buckets']['cs_binaries'])
end end
end end

311
lib/remote_storage/swift.rb Normal file
View File

@ -0,0 +1,311 @@
require "rest_client"
require "json"
require "cgi"
require "active_support/core_ext/time/conversions"
require "active_support/core_ext/numeric/time"
require "redis"
module RemoteStorage
class Swift
attr_accessor :settings, :server
def initialize(settings, server)
self.settings = settings
self.server = server
end
def authorize_request(user, directory, token, listing=false)
request_method = server.env["REQUEST_METHOD"]
if directory.split("/").first == "public"
return true if ["GET", "HEAD"].include?(request_method) && !listing
end
authorizations = redis.smembers("authorizations:#{user}:#{token}")
permission = directory_permission(authorizations, directory)
server.halt 401 unless permission
if ["PUT", "DELETE"].include? request_method
server.halt 401 unless permission == "rw"
end
end
def get_head(user, directory, key)
url = url_for(user, directory, key)
res = do_head_request(url)
set_response_headers(res)
rescue RestClient::ResourceNotFound
server.halt 404
end
def get_data(user, directory, key)
url = url_for(user, directory, key)
res = do_get_request(url)
set_response_headers(res)
none_match = (server.env["HTTP_IF_NONE_MATCH"] || "").split(",").map(&:strip)
server.halt 304 if none_match.include? %Q("#{res.headers[:etag]}")
return res.body
rescue RestClient::ResourceNotFound
server.halt 404
end
def get_head_directory_listing(user, directory)
res = do_head_request("#{container_url_for(user)}/#{directory}/")
server.headers["Content-Type"] = "application/json"
server.headers["ETag"] = %Q("#{res.headers[:etag]}")
rescue RestClient::ResourceNotFound
server.halt 404
end
def get_directory_listing(user, directory)
server.headers["Content-Type"] = "application/json"
do_head_request("#{container_url_for(user)}/#{directory}/") do |response|
if response.code == 404
return directory_listing([]).to_json
else
server.headers["ETag"] = %Q("#{response.headers[:etag]}")
none_match = (server.env["HTTP_IF_NONE_MATCH"] || "").split(",").map(&:strip)
server.halt 304 if none_match.include? %Q("#{response.headers[:etag]}")
end
end
res = do_get_request("#{container_url_for(user)}/?format=json&path=#{directory}/")
if body = JSON.parse(res.body)
listing = directory_listing(body)
else
puts "listing not JSON"
end
listing.to_json
end
def put_data(user, directory, key, data, content_type)
server.halt 409 if has_name_collision?(user, directory, key)
url = url_for(user, directory, key)
if required_match = server.env["HTTP_IF_MATCH"]
do_head_request(url) do |response|
server.halt 412 unless required_match == %Q("#{response.headers[:etag]}")
end
end
if server.env["HTTP_IF_NONE_MATCH"] == "*"
do_head_request(url) do |response|
server.halt 412 unless response.code == 404
end
end
res = do_put_request(url, data, content_type)
if update_dir_objects(user, directory)
server.headers["ETag"] = %Q("#{res.headers[:etag]}")
server.halt 200
else
server.halt 500
end
end
def delete_data(user, directory, key)
url = url_for(user, directory, key)
if required_match = server.env["HTTP_IF_MATCH"]
do_head_request(url) do |response|
server.halt 412 unless required_match == %Q("#{response.headers[:etag]}")
end
end
do_delete_request(url)
delete_dir_objects(user, directory)
server.halt 200
rescue RestClient::ResourceNotFound
server.halt 404
end
private
def set_response_headers(response)
server.headers["ETag"] = %Q("#{response.headers[:etag]}")
server.headers["Content-Type"] = response.headers[:content_type]
server.headers["Content-Length"] = response.headers[:content_length]
server.headers["Last-Modified"] = response.headers[:last_modified]
end
def extract_category(directory)
if directory.match(/^public\//)
"public/#{directory.split('/')[1]}"
else
directory.split('/').first
end
end
def directory_permission(authorizations, directory)
authorizations = authorizations.map do |auth|
auth.index(":") ? auth.split(":") : [auth, "rw"]
end
authorizations = Hash[*authorizations.flatten]
permission = authorizations[""]
authorizations.each do |key, value|
if directory.match(/^(public\/)?#{key}(\/|$)/)
if permission.nil? || permission == "r"
permission = value
end
return permission if permission == "rw"
end
end
permission
end
def directory_listing(res_body)
listing = {
"@context" => "http://remotestorage.io/spec/folder-description",
"items" => {}
}
res_body.each do |entry|
name = entry["name"].gsub("#{File.dirname(entry["name"])}/", '')
if name[-1] == "/"
listing["items"].merge!({
name => {
"ETag" => entry["hash"],
}
})
else
listing["items"].merge!({
name => {
"ETag" => entry["hash"],
"Content-Type" => entry["content_type"],
"Content-Length" => entry["bytes"]
}
})
end
end
listing
end
def has_name_collision?(user, directory, key)
# check for existing directory with the same name as the document
do_head_request("#{container_url_for(user)}/#{directory}/#{key}/") do |res|
return true if res.code == 200
end
# check for existing documents with the same name as one of the parent directories
parent_directories_for(directory).each do |dir|
do_head_request("#{container_url_for(user)}/#{dir}/") do |res_dir|
if res_dir.code == 200
return false
else
do_head_request("#{container_url_for(user)}/#{dir}") do |res_key|
if res_key.code == 200
return true
else
next
end
end
end
end
end
false
end
def parent_directories_for(directory)
directories = directory.split("/")
parent_directories = []
while directories.any?
parent_directories << directories.join("/")
directories.pop
end
parent_directories
end
def update_dir_objects(user, directory)
timestamp = (Time.now.to_f * 1000).to_i
parent_directories_for(directory).each do |dir|
do_put_request("#{container_url_for(user)}/#{dir}/", timestamp.to_s, "text/plain")
end
true
rescue
parent_directories_for(directory).each do |dir|
do_delete_request("#{container_url_for(user)}/#{dir}/") rescue false
end
false
end
def delete_dir_objects(user, directory)
parent_directories_for(directory).each do |dir|
if dir_empty?(user, dir)
do_delete_request("#{container_url_for(user)}/#{dir}/")
else
timestamp = (Time.now.to_f * 1000).to_i
do_put_request("#{container_url_for(user)}/#{dir}/", timestamp.to_s, "text/plain")
end
end
end
def dir_empty?(user, dir)
do_get_request("#{container_url_for(user)}/?format=plain&limit=1&path=#{dir}/") do |res|
return res.headers[:content_length] == "0"
end
end
def container_url_for(user)
"#{base_url}/#{container_for(user)}"
end
def url_for(user, directory, key)
"#{container_url_for(user)}/#{directory}/#{key}"
end
def base_url
@base_url ||= settings.swift["host"]
end
def container_for(user)
"rs:#{settings.environment.to_s.chars.first}:#{user}"
end
def default_headers
@default_headers ||= {"x-auth-token" => settings.swift["token"]}
end
def do_put_request(url, data, content_type)
RestClient.put(url, data, default_headers.merge({content_type: content_type}))
end
def do_get_request(url, &block)
RestClient.get(url, default_headers, &block)
end
def do_head_request(url, &block)
RestClient.head(url, default_headers, &block)
end
def do_delete_request(url)
RestClient.delete(url, default_headers)
end
def redis
@redis ||= Redis.new(host: settings.redis["host"], port: settings.redis["port"])
end
end
end

View File

@ -5,6 +5,7 @@ require "sinatra/base"
require 'sinatra/config_file' require 'sinatra/config_file'
require "sinatra/reloader" require "sinatra/reloader"
require "remote_storage/riak" require "remote_storage/riak"
require "remote_storage/swift"
class LiquorCabinet < Sinatra::Base class LiquorCabinet < Sinatra::Base
@ -23,11 +24,22 @@ class LiquorCabinet < Sinatra::Base
configure :development do configure :development do
register Sinatra::Reloader register Sinatra::Reloader
also_reload "lib/remote_storage/*.rb"
enable :logging enable :logging
end end
configure :production, :staging do configure :production, :staging do
require "rack/common_logger" require "rack/common_logger"
if ENV['SENTRY_DSN']
require "raven"
Raven.configure do |config|
config.dsn = ENV['SENTRY_DSN']
config.tags = { environment: settings.environment.to_s }
end
use Raven::Rack
end
end end
# #
@ -62,17 +74,19 @@ class LiquorCabinet < Sinatra::Base
end end
["/:user/*/:key", "/:user/:key"].each do |path| ["/:user/*/:key", "/:user/:key"].each do |path|
get path do
storage.get_data(@user, @directory, @key)
end
head path do head path do
storage.get_head(@user, @directory, @key) storage.get_head(@user, @directory, @key)
end end
get path do
storage.get_data(@user, @directory, @key)
end
put path do put path do
data = request.body.read data = request.body.read
halt 422 unless env['CONTENT_TYPE']
if env['CONTENT_TYPE'] == "application/x-www-form-urlencoded" if env['CONTENT_TYPE'] == "application/x-www-form-urlencoded"
content_type = "text/plain; charset=utf-8" content_type = "text/plain; charset=utf-8"
else else
@ -88,23 +102,29 @@ class LiquorCabinet < Sinatra::Base
end end
["/:user/*/", "/:user/"].each do |path| ["/:user/*/", "/:user/"].each do |path|
get path do
storage.get_directory_listing(@user, @directory)
end
head path do head path do
storage.get_head_directory_listing(@user, @directory) storage.get_head_directory_listing(@user, @directory)
end end
get path do
storage.get_directory_listing(@user, @directory)
end
end end
private private
def storage def storage
@storage ||= begin @storage ||= begin
if settings.riak if settings.respond_to? :riak
RemoteStorage::Riak.new(settings.riak, self) RemoteStorage::Riak.new(settings, self)
# elsif settings.redis elsif settings.respond_to? :swift
# include RemoteStorage::Redis settings.swift["token"] = File.read("tmp/swift_token.txt")
RemoteStorage::Swift.new(settings, self)
else
puts <<-EOF
You need to set one storage backend in your config.yml file.
Riak and Swift are currently supported. See config.yml.example.
EOF
end end
end end
end end

File diff suppressed because it is too large Load Diff

View File

@ -28,8 +28,7 @@ def write_last_response_to_file(filename = "last_response.html")
end end
alias context describe alias context describe
if app.settings.respond_to? :riak
if app.settings.riak
::Riak.disable_list_keys_warnings = true ::Riak.disable_list_keys_warnings = true
def client def client