Support directory listings
This commit is contained in:
parent
5155d36de4
commit
ebe499211a
@ -1,5 +1,6 @@
|
|||||||
require "riak"
|
require "riak"
|
||||||
require "json"
|
require "json"
|
||||||
|
require "cgi"
|
||||||
|
|
||||||
module RemoteStorage
|
module RemoteStorage
|
||||||
module Riak
|
module Riak
|
||||||
@ -12,6 +13,10 @@ module RemoteStorage
|
|||||||
@data_bucket ||= client.bucket("user_data")
|
@data_bucket ||= client.bucket("user_data")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def directory_bucket
|
||||||
|
@directory_bucket ||= client.bucket("rs_directories")
|
||||||
|
end
|
||||||
|
|
||||||
def authorize_request(user, category, token)
|
def authorize_request(user, category, token)
|
||||||
request_method = env["REQUEST_METHOD"]
|
request_method = env["REQUEST_METHOD"]
|
||||||
return true if category.split("/").first == "public" && request_method == "GET"
|
return true if category.split("/").first == "public" && request_method == "GET"
|
||||||
@ -41,6 +46,19 @@ module RemoteStorage
|
|||||||
halt 404
|
halt 404
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_directory_listing(user, directory)
|
||||||
|
directory_object = directory_bucket.get("#{user}:#{directory}")
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
headers["Last-Modified"] = directory_object.last_modified.to_s(:rfc822)
|
||||||
|
|
||||||
|
listing = directory_listing(user, directory)
|
||||||
|
|
||||||
|
return listing.to_json
|
||||||
|
rescue ::Riak::HTTPFailedRequest
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
return "{}"
|
||||||
|
end
|
||||||
|
|
||||||
def put_data(user, category, key, data, content_type=nil)
|
def put_data(user, category, key, data, content_type=nil)
|
||||||
object = data_bucket.new("#{user}:#{category}:#{key}")
|
object = data_bucket.new("#{user}:#{category}:#{key}")
|
||||||
object.content_type = content_type || "text/plain; charset=utf-8"
|
object.content_type = content_type || "text/plain; charset=utf-8"
|
||||||
@ -50,14 +68,33 @@ module RemoteStorage
|
|||||||
else
|
else
|
||||||
object.raw_data = data
|
object.raw_data = data
|
||||||
end
|
end
|
||||||
object.indexes.merge!({:user_id_bin => [user]})
|
object.indexes.merge!({:user_id_bin => [user],
|
||||||
|
:directory_bin => [category]})
|
||||||
object.store
|
object.store
|
||||||
|
|
||||||
|
update_directory_object(user, category)
|
||||||
rescue ::Riak::HTTPFailedRequest
|
rescue ::Riak::HTTPFailedRequest
|
||||||
halt 422
|
halt 422
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_directory_object(user, category)
|
||||||
|
if category.match /\//
|
||||||
|
parent_directory = category[0..category.rindex("/")-1]
|
||||||
|
end
|
||||||
|
directory = directory_bucket.new("#{user}:#{category}")
|
||||||
|
directory.raw_data = ""
|
||||||
|
directory.indexes.merge!({:user_id_bin => [user]})
|
||||||
|
if parent_directory
|
||||||
|
directory.indexes.merge!({:directory_bin => [parent_directory]})
|
||||||
|
end
|
||||||
|
directory.store
|
||||||
|
end
|
||||||
|
|
||||||
def delete_data(user, category, key)
|
def delete_data(user, category, key)
|
||||||
riak_response = data_bucket.delete("#{user}:#{category}:#{key}")
|
riak_response = data_bucket.delete("#{user}:#{category}:#{key}")
|
||||||
|
if directory_entries(user, category).empty?
|
||||||
|
directory_bucket.delete "#{user}:#{category}"
|
||||||
|
end
|
||||||
halt riak_response[:code]
|
halt riak_response[:code]
|
||||||
rescue ::Riak::HTTPFailedRequest
|
rescue ::Riak::HTTPFailedRequest
|
||||||
halt 404
|
halt 404
|
||||||
@ -88,5 +125,59 @@ module RemoteStorage
|
|||||||
permission
|
permission
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def directory_listing(user, directory)
|
||||||
|
listing = {}
|
||||||
|
|
||||||
|
sub_directories(user, directory).each do |entry|
|
||||||
|
timestamp = DateTime.rfc2822(entry["last_modified"]).to_time.to_i
|
||||||
|
listing.merge!({ "#{entry["name"]}/" => timestamp })
|
||||||
|
end
|
||||||
|
|
||||||
|
directory_entries(user, directory).each do |entry|
|
||||||
|
timestamp = DateTime.rfc2822(entry["last_modified"]).to_time.to_i
|
||||||
|
listing.merge!({ entry["name"] => timestamp })
|
||||||
|
end
|
||||||
|
|
||||||
|
listing
|
||||||
|
end
|
||||||
|
|
||||||
|
def directory_entries(user, directory)
|
||||||
|
map_query = <<-EOH
|
||||||
|
function(v){
|
||||||
|
keys = v.key.split(':');
|
||||||
|
key_name = keys[keys.length-1];
|
||||||
|
last_modified_date = v.values[0]['metadata']['X-Riak-Last-Modified'];
|
||||||
|
return [{
|
||||||
|
name: key_name,
|
||||||
|
last_modified: last_modified_date,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
EOH
|
||||||
|
objects = ::Riak::MapReduce.new(client).
|
||||||
|
index("user_data", "user_id_bin", user).
|
||||||
|
index("user_data", "directory_bin", directory).
|
||||||
|
map(map_query, :keep => true).
|
||||||
|
run
|
||||||
|
end
|
||||||
|
|
||||||
|
def sub_directories(user, directory)
|
||||||
|
map_query = <<-EOH
|
||||||
|
function(v){
|
||||||
|
keys = v.key.split(':');
|
||||||
|
key_name = keys[keys.length-1];
|
||||||
|
last_modified_date = v.values[0]['metadata']['X-Riak-Last-Modified'];
|
||||||
|
return [{
|
||||||
|
name: key_name,
|
||||||
|
last_modified: last_modified_date,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
EOH
|
||||||
|
objects = ::Riak::MapReduce.new(client).
|
||||||
|
index("rs_directories", "user_id_bin", user).
|
||||||
|
index("rs_directories", "directory_bin", directory).
|
||||||
|
map(map_query, :keep => true).
|
||||||
|
run
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -29,20 +29,26 @@ class LiquorCabinet < Sinatra::Base
|
|||||||
disable :logging
|
disable :logging
|
||||||
end
|
end
|
||||||
|
|
||||||
before "/:user/*/:key" do
|
["/:user/*/:key", "/:user/*/"].each do |path|
|
||||||
|
before path do
|
||||||
headers 'Access-Control-Allow-Origin' => '*',
|
headers 'Access-Control-Allow-Origin' => '*',
|
||||||
'Access-Control-Allow-Methods' => 'GET, PUT, DELETE',
|
'Access-Control-Allow-Methods' => 'GET, PUT, DELETE',
|
||||||
'Access-Control-Allow-Headers' => 'Authorization, Content-Type, Origin'
|
'Access-Control-Allow-Headers' => 'Authorization, Content-Type, Origin'
|
||||||
headers['Access-Control-Allow-Origin'] = env["HTTP_ORIGIN"] if env["HTTP_ORIGIN"]
|
headers['Access-Control-Allow-Origin'] = env["HTTP_ORIGIN"] if env["HTTP_ORIGIN"]
|
||||||
|
|
||||||
@user, @category, @key = params[:user], params[:splat].first, params[:key]
|
@user, @directory, @key = params[:user], params[:splat].first, params[:key]
|
||||||
token = env["HTTP_AUTHORIZATION"] ? env["HTTP_AUTHORIZATION"].split(" ")[1] : ""
|
token = env["HTTP_AUTHORIZATION"] ? env["HTTP_AUTHORIZATION"].split(" ")[1] : ""
|
||||||
|
|
||||||
authorize_request(@user, @category, token) unless request.options?
|
authorize_request(@user, @directory, token) unless request.options?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/:user/*/:key" do
|
get "/:user/*/:key" do
|
||||||
get_data(@user, @category, @key)
|
get_data(@user, @directory, @key)
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/:user/*/" do
|
||||||
|
get_directory_listing(@user, @directory)
|
||||||
end
|
end
|
||||||
|
|
||||||
put "/:user/*/:key" do
|
put "/:user/*/:key" do
|
||||||
@ -54,11 +60,11 @@ class LiquorCabinet < Sinatra::Base
|
|||||||
content_type = env['CONTENT_TYPE']
|
content_type = env['CONTENT_TYPE']
|
||||||
end
|
end
|
||||||
|
|
||||||
put_data(@user, @category, @key, data, content_type)
|
put_data(@user, @directory, @key, data, content_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
delete "/:user/*/:key" do
|
delete "/:user/*/:key" do
|
||||||
delete_data(@user, @category, @key)
|
delete_data(@user, @directory, @key)
|
||||||
end
|
end
|
||||||
|
|
||||||
options "/:user/*/:key" do
|
options "/:user/*/:key" do
|
||||||
|
153
spec/directories_spec.rb
Normal file
153
spec/directories_spec.rb
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
require_relative "spec_helper"
|
||||||
|
|
||||||
|
describe "Directories" do
|
||||||
|
include Rack::Test::Methods
|
||||||
|
include RemoteStorage::Riak
|
||||||
|
|
||||||
|
before do
|
||||||
|
purge_all_buckets
|
||||||
|
|
||||||
|
auth = auth_bucket.new("jimmy:123")
|
||||||
|
auth.data = ["documents:r", "tasks:rw"]
|
||||||
|
auth.store
|
||||||
|
|
||||||
|
header "Authorization", "Bearer 123"
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET listing" do
|
||||||
|
|
||||||
|
before do
|
||||||
|
put "/jimmy/tasks/home/foo", "do the laundry"
|
||||||
|
put "/jimmy/tasks/home/bar", "do the laundry"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "lists the objects with a timestamp of the last modification" do
|
||||||
|
get "/jimmy/tasks/home/"
|
||||||
|
|
||||||
|
# File.open("stacktrace.html", "w") do |f|
|
||||||
|
# f.write last_response.body
|
||||||
|
# end
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
last_response.content_type.must_equal "application/json"
|
||||||
|
|
||||||
|
content = JSON.parse(last_response.body)
|
||||||
|
content["foo"].to_s.must_match /\d+/
|
||||||
|
content["foo"].to_s.length.must_be :>=, 10
|
||||||
|
end
|
||||||
|
|
||||||
|
it "has a Last-Modifier header set" do
|
||||||
|
get "/jimmy/tasks/home/"
|
||||||
|
|
||||||
|
last_response.headers["Last-Modified"].wont_be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "has CORS headers set" do
|
||||||
|
get "/jimmy/tasks/home/"
|
||||||
|
|
||||||
|
last_response.headers["Access-Control-Allow-Origin"].must_equal "*"
|
||||||
|
last_response.headers["Access-Control-Allow-Methods"].must_equal "GET, PUT, DELETE"
|
||||||
|
last_response.headers["Access-Control-Allow-Headers"].must_equal "Authorization, Content-Type, Origin"
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with sub-directories" do
|
||||||
|
before do
|
||||||
|
put "/jimmy/tasks/home/laundry", "do the laundry"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "lists the containing objects as well as the direct sub-directories" do
|
||||||
|
get "/jimmy/tasks/"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
content = JSON.parse(last_response.body)
|
||||||
|
# p content
|
||||||
|
# dir = directory_bucket.get("jimmy:tasks/home")
|
||||||
|
# puts dir.indexes.inspect
|
||||||
|
content["home/"].to_s.must_match /\d+/
|
||||||
|
end
|
||||||
|
|
||||||
|
context "for a sub-directory" do
|
||||||
|
before do
|
||||||
|
put "/jimmy/tasks/home/laundry", "do the laundry"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "lists the objects with timestamp" do
|
||||||
|
get "/jimmy/tasks/home/"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
|
||||||
|
content = JSON.parse(last_response.body)
|
||||||
|
content["laundry"].to_s.must_match /\d+/
|
||||||
|
content["laundry"].to_s.length.must_be :>=, 10
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "for an empty or absent directory" do
|
||||||
|
it "returns an empty listing" do
|
||||||
|
get "/jimmy/documents/notfound/"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
last_response.body.must_equal "{}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "directory object" do
|
||||||
|
describe "PUT file" do
|
||||||
|
context "no existing directory object" do
|
||||||
|
it "creates a new directory object" do
|
||||||
|
put "/jimmy/tasks/home/trash", "take out the trash"
|
||||||
|
|
||||||
|
object = directory_bucket.get("jimmy:tasks/home")
|
||||||
|
object.last_modified.wont_be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "sets the correct index for the directory object" do
|
||||||
|
put "/jimmy/tasks/home/trash", "take out the trash"
|
||||||
|
|
||||||
|
object = directory_bucket.get("jimmy:tasks/home")
|
||||||
|
object.indexes["directory_bin"].must_include "tasks"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "existing directory object" do
|
||||||
|
before do
|
||||||
|
@directory = directory_bucket.new("jimmy:tasks/home")
|
||||||
|
@directory.content_type = "text/plain"
|
||||||
|
@directory.raw_data = ""
|
||||||
|
@directory.store
|
||||||
|
@old_timestamp = @directory.reload.last_modified
|
||||||
|
end
|
||||||
|
|
||||||
|
it "updates the timestamp of the directory" do
|
||||||
|
sleep 1
|
||||||
|
put "/jimmy/tasks/home/trash", "take out the trash"
|
||||||
|
|
||||||
|
@directory.reload
|
||||||
|
@directory.last_modified.must_be :>, @old_timestamp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "DELETE file" do
|
||||||
|
context "last file in directory" do
|
||||||
|
before do
|
||||||
|
directory_bucket.delete("jimmy:tasks")
|
||||||
|
put "/jimmy/tasks/trash", "take out the trash"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "deletes the directory object" do
|
||||||
|
delete "/jimmy/tasks/trash"
|
||||||
|
|
||||||
|
last_response.status.must_equal 204
|
||||||
|
|
||||||
|
lambda {
|
||||||
|
directory_bucket.get("jimmy:tasks")
|
||||||
|
}.must_raise Riak::HTTPFailedRequest
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
@ -14,3 +14,33 @@ ENV["RACK_ENV"] = "test"
|
|||||||
config = File.read(File.expand_path('../config.yml', File.dirname(__FILE__)))
|
config = File.read(File.expand_path('../config.yml', File.dirname(__FILE__)))
|
||||||
riak_config = YAML.load(config)[ENV['RACK_ENV']]['riak'].symbolize_keys
|
riak_config = YAML.load(config)[ENV['RACK_ENV']]['riak'].symbolize_keys
|
||||||
set :riak_config, riak_config
|
set :riak_config, riak_config
|
||||||
|
|
||||||
|
::Riak.disable_list_keys_warnings = true
|
||||||
|
|
||||||
|
def app
|
||||||
|
LiquorCabinet
|
||||||
|
end
|
||||||
|
|
||||||
|
def storage_client
|
||||||
|
@storage_client ||= ::Riak::Client.new(settings.riak_config)
|
||||||
|
end
|
||||||
|
|
||||||
|
def data_bucket
|
||||||
|
@data_bucket ||= storage_client.bucket("user_data")
|
||||||
|
end
|
||||||
|
|
||||||
|
def auth_bucket
|
||||||
|
@auth_bucket ||= storage_client.bucket("authorizations")
|
||||||
|
end
|
||||||
|
|
||||||
|
def directory_bucket
|
||||||
|
@directory_bucket ||= storage_client.bucket("rs_directories")
|
||||||
|
end
|
||||||
|
|
||||||
|
def purge_all_buckets
|
||||||
|
[data_bucket, directory_bucket, auth_bucket].each do |bucket|
|
||||||
|
bucket.keys.each {|key| bucket.delete key}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
alias context describe
|
||||||
|
Loading…
x
Reference in New Issue
Block a user