filezor 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/bin/filezor +22 -0
- data/config/filezor.ru +10 -0
- data/config/filezor.yml +10 -0
- data/filezor.gemspec +78 -0
- data/lib/filezor.rb +5 -0
- data/lib/filezor/client.rb +106 -0
- data/lib/filezor/file.rb +11 -0
- data/lib/filezor/server.rb +210 -0
- data/lib/filezor/util.rb +23 -0
- data/spec/filezor/client_spec.rb +107 -0
- data/spec/filezor/server_spec.rb +213 -0
- data/spec/filezor/util_spec.rb +24 -0
- data/spec/fixtures/.password +1 -0
- data/spec/fixtures/a.txt +1 -0
- data/spec/fixtures/b.txt +0 -0
- data/spec/fixtures/z/f.txt +0 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +21 -0
- metadata +127 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Kyle Maxwell
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
= filezor
|
2
|
+
|
3
|
+
Description goes here.
|
4
|
+
|
5
|
+
== Note on Patches/Pull Requests
|
6
|
+
|
7
|
+
* Fork the project.
|
8
|
+
* Make your feature addition or bug fix.
|
9
|
+
* Add tests for it. This is important so I don't break it in a
|
10
|
+
future version unintentionally.
|
11
|
+
* Commit, do not mess with rakefile, version, or history.
|
12
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
13
|
+
* Send me a pull request. Bonus points for topic branches.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2010 Kyle Maxwell. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "filezor"
|
8
|
+
gem.summary = %Q{pure ruby file sync}
|
9
|
+
gem.description = %Q{pure ruby file sync}
|
10
|
+
gem.email = "kyle@kylemaxwell.com"
|
11
|
+
gem.homepage = "http://github.com/fizx/filezor"
|
12
|
+
gem.authors = ["Kyle Maxwell"]
|
13
|
+
gem.add_dependency "trollop"
|
14
|
+
gem.add_dependency "rest-client", "~> 1.3"
|
15
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
16
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
|
+
end
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
def sys(s);puts s; system s; end
|
24
|
+
|
25
|
+
require 'spec/rake/spectask'
|
26
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
27
|
+
spec.libs << 'lib' << 'spec'
|
28
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
29
|
+
end
|
30
|
+
|
31
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
32
|
+
spec.libs << 'lib' << 'spec'
|
33
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
34
|
+
spec.rcov = true
|
35
|
+
end
|
36
|
+
|
37
|
+
task :spec => :check_dependencies
|
38
|
+
|
39
|
+
task :default => :spec
|
40
|
+
|
41
|
+
task :push => :build do
|
42
|
+
abort "No HOST" unless ENV["HOST"]
|
43
|
+
version = File.exist?('VERSION') ? File.read('VERSION').strip : ""
|
44
|
+
sys "scp pkg/filezor-#{version}.gem #{ENV["HOST"]}:~"
|
45
|
+
sys "ssh #{ENV["HOST"]} 'sudo gem install filezor-#{version}.gem'"
|
46
|
+
end
|
47
|
+
|
48
|
+
require 'rake/rdoctask'
|
49
|
+
Rake::RDocTask.new do |rdoc|
|
50
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
51
|
+
|
52
|
+
rdoc.rdoc_dir = 'rdoc'
|
53
|
+
rdoc.title = "filezor #{version}"
|
54
|
+
rdoc.rdoc_files.include('README*')
|
55
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
56
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.3.2
|
data/bin/filezor
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$: << File.dirname(__FILE__) + "/../lib"
|
3
|
+
require "rubygems"
|
4
|
+
require "trollop"
|
5
|
+
require "filezor"
|
6
|
+
|
7
|
+
usage = "Usage: #{$0} [options]"
|
8
|
+
opts = Trollop::options do
|
9
|
+
banner <<-EOS
|
10
|
+
Runs a filezor server
|
11
|
+
|
12
|
+
#{usage}
|
13
|
+
|
14
|
+
EOS
|
15
|
+
opt :root, "Filesystem root", :default => "/tmp"
|
16
|
+
opt :caching, "Turn caching on"
|
17
|
+
end
|
18
|
+
|
19
|
+
Filezor::Server.root = opts[:root]
|
20
|
+
Filezor::Server.caching = opts[:caching]
|
21
|
+
|
22
|
+
Filezor::Server.run!
|
data/config/filezor.ru
ADDED
data/config/filezor.yml
ADDED
data/filezor.gemspec
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{filezor}
|
8
|
+
s.version = "1.3.2"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Kyle Maxwell"]
|
12
|
+
s.date = %q{2010-09-06}
|
13
|
+
s.default_executable = %q{filezor}
|
14
|
+
s.description = %q{pure ruby file sync}
|
15
|
+
s.email = %q{kyle@kylemaxwell.com}
|
16
|
+
s.executables = ["filezor"]
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE",
|
19
|
+
"README.rdoc"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".document",
|
23
|
+
".gitignore",
|
24
|
+
"LICENSE",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"bin/filezor",
|
29
|
+
"config/filezor.ru",
|
30
|
+
"config/filezor.yml",
|
31
|
+
"filezor.gemspec",
|
32
|
+
"lib/filezor.rb",
|
33
|
+
"lib/filezor/client.rb",
|
34
|
+
"lib/filezor/file.rb",
|
35
|
+
"lib/filezor/server.rb",
|
36
|
+
"lib/filezor/util.rb",
|
37
|
+
"spec/filezor/client_spec.rb",
|
38
|
+
"spec/filezor/server_spec.rb",
|
39
|
+
"spec/filezor/util_spec.rb",
|
40
|
+
"spec/fixtures/.password",
|
41
|
+
"spec/fixtures/a.txt",
|
42
|
+
"spec/fixtures/b.txt",
|
43
|
+
"spec/fixtures/z/f.txt",
|
44
|
+
"spec/spec.opts",
|
45
|
+
"spec/spec_helper.rb"
|
46
|
+
]
|
47
|
+
s.homepage = %q{http://github.com/fizx/filezor}
|
48
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
49
|
+
s.require_paths = ["lib"]
|
50
|
+
s.rubygems_version = %q{1.3.6}
|
51
|
+
s.summary = %q{pure ruby file sync}
|
52
|
+
s.test_files = [
|
53
|
+
"spec/filezor/client_spec.rb",
|
54
|
+
"spec/filezor/server_spec.rb",
|
55
|
+
"spec/filezor/util_spec.rb",
|
56
|
+
"spec/spec_helper.rb"
|
57
|
+
]
|
58
|
+
|
59
|
+
if s.respond_to? :specification_version then
|
60
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
61
|
+
s.specification_version = 3
|
62
|
+
|
63
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
64
|
+
s.add_runtime_dependency(%q<trollop>, [">= 0"])
|
65
|
+
s.add_runtime_dependency(%q<rest-client>, ["~> 1.3"])
|
66
|
+
s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
|
67
|
+
else
|
68
|
+
s.add_dependency(%q<trollop>, [">= 0"])
|
69
|
+
s.add_dependency(%q<rest-client>, ["~> 1.3"])
|
70
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
71
|
+
end
|
72
|
+
else
|
73
|
+
s.add_dependency(%q<trollop>, [">= 0"])
|
74
|
+
s.add_dependency(%q<rest-client>, ["~> 1.3"])
|
75
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
data/lib/filezor.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "cgi"
|
3
|
+
require "set"
|
4
|
+
require "restclient"
|
5
|
+
require "filezor/file"
|
6
|
+
require "json"
|
7
|
+
|
8
|
+
class Filezor::Client
|
9
|
+
class Unauthorized < RuntimeError; end
|
10
|
+
class InvalidArgument < RuntimeError; end
|
11
|
+
|
12
|
+
attr_reader :options
|
13
|
+
|
14
|
+
def initialize(host, port, password, options = {})
|
15
|
+
@root = "http://admin:#{password}@#{host}:#{port}"
|
16
|
+
@options ||= options
|
17
|
+
@options.update JSON.parse(post("info", options.to_json, "/"))
|
18
|
+
end
|
19
|
+
|
20
|
+
# This method acts like an rsync --delete. It deletes all files in the root,
|
21
|
+
# and then puts the new files in. TODO: make atomic
|
22
|
+
def sync(*files)
|
23
|
+
options = files.last.is_a?(Hash) ? files.pop : {}
|
24
|
+
files = files.flatten.uniq
|
25
|
+
test(files)
|
26
|
+
#
|
27
|
+
# files.each do |f|
|
28
|
+
# f.path = File.join(root, f.path)
|
29
|
+
# end
|
30
|
+
cached_md5s = caching? ? Set.new(cached(files)) : []
|
31
|
+
params = files.inject({}) do |memo, file|
|
32
|
+
memo[file.path] = cached_md5s.include?(file.md5) ? file.md5 : file.tempfile
|
33
|
+
memo
|
34
|
+
end
|
35
|
+
|
36
|
+
params["--delete"] = options[:delete] if options[:delete]
|
37
|
+
params["--root"] = options[:root] if options[:root]
|
38
|
+
post("", params)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Simple file upload
|
42
|
+
def put(*files)
|
43
|
+
test(files)
|
44
|
+
params = files.flatten.inject({}) do |memo, file|
|
45
|
+
memo[file.path] = file.tempfile
|
46
|
+
memo
|
47
|
+
end
|
48
|
+
post("", params)
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_options(options = {})
|
52
|
+
@options = JSON.parse(post("info", options.to_json, "/"))
|
53
|
+
end
|
54
|
+
|
55
|
+
def ping
|
56
|
+
get("ping", "/").code == 200
|
57
|
+
end
|
58
|
+
|
59
|
+
def cached(*files)
|
60
|
+
JSON.parse(post("cached", files.flatten.map{|f| f.md5 }.to_json, "/"))
|
61
|
+
end
|
62
|
+
|
63
|
+
def get(path, prefix = "/file/")
|
64
|
+
_get("#{http_root}#{prefix}#{path}")
|
65
|
+
rescue RestClient::Unauthorized
|
66
|
+
raise Unauthorized
|
67
|
+
end
|
68
|
+
|
69
|
+
def post(path, params, prefix = "/file/")
|
70
|
+
_post("#{http_root}#{prefix}#{path}", params)
|
71
|
+
rescue RestClient::Unauthorized
|
72
|
+
raise Unauthorized
|
73
|
+
end
|
74
|
+
|
75
|
+
def http_root
|
76
|
+
"#{@root}#{app_root}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def app_root
|
80
|
+
options["app_root"] || ""
|
81
|
+
end
|
82
|
+
|
83
|
+
def caching?
|
84
|
+
options["caching"]
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def _get(*args)
|
90
|
+
puts args.inspect if ENV["FILEZOR_DEBUG"]
|
91
|
+
RestClient.get(*args)
|
92
|
+
end
|
93
|
+
|
94
|
+
def _post(*args)
|
95
|
+
puts args.inspect if ENV["FILEZOR_DEBUG"]
|
96
|
+
RestClient.post(*args)
|
97
|
+
end
|
98
|
+
|
99
|
+
def test(*files)
|
100
|
+
files.flatten.each do |file|
|
101
|
+
unless file.is_a?(Filezor::File)
|
102
|
+
raise InvalidArgument.new(file.inspect)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/filezor/file.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "filezor/util"
|
2
|
+
# A File that knows its MD5 hash, and target upload path.
|
3
|
+
class Filezor::File
|
4
|
+
attr_accessor :tempfile, :md5, :path
|
5
|
+
|
6
|
+
def initialize(tempfile, path, hash = nil)
|
7
|
+
@tempfile = tempfile
|
8
|
+
@path = path
|
9
|
+
@md5 = hash || Filezor::Util::md5(tempfile)
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require "sinatra"
|
2
|
+
require "cgi"
|
3
|
+
require "fileutils"
|
4
|
+
require "json"
|
5
|
+
require "filezor/util"
|
6
|
+
class Filezor::Server < Sinatra::Base
|
7
|
+
class BadRequest < RuntimeError; end
|
8
|
+
include FileUtils
|
9
|
+
include Filezor::Util
|
10
|
+
|
11
|
+
PATH_ROUTE = %r[/file(/.+)]
|
12
|
+
PATH_ROUTE_ROOT = %r[/file/?]
|
13
|
+
CACHED = %r[/cached/?]
|
14
|
+
PASSWORD = %r[/auth/?]
|
15
|
+
INFO = %r[/info/?]
|
16
|
+
PING = %r[/ping/?]
|
17
|
+
|
18
|
+
use Rack::Auth::Basic do |username, password|
|
19
|
+
authenticate(username, password)
|
20
|
+
end
|
21
|
+
|
22
|
+
use Rack::CommonLogger
|
23
|
+
|
24
|
+
get INFO do
|
25
|
+
JSON.generate("caching" => caching?, "root" => root)
|
26
|
+
end
|
27
|
+
|
28
|
+
post INFO do
|
29
|
+
json = JSON.parse(request.body.read)
|
30
|
+
self.class.root = json["root"] unless json["root"].nil?
|
31
|
+
self.class.caching = json["caching"] unless json["caching"].nil?
|
32
|
+
JSON.generate("caching" => caching?, "root" => root)
|
33
|
+
end
|
34
|
+
|
35
|
+
get PATH_ROUTE do
|
36
|
+
path = path_of(params[:captures].first)
|
37
|
+
if File.file?(path)
|
38
|
+
File.read(path)
|
39
|
+
else
|
40
|
+
404
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
get PING do
|
45
|
+
"ok"
|
46
|
+
end
|
47
|
+
|
48
|
+
delete PATH_ROUTE do
|
49
|
+
path = path_of(params[:captures].first)
|
50
|
+
rm_rf path
|
51
|
+
"ok"
|
52
|
+
end
|
53
|
+
|
54
|
+
post PATH_ROUTE_ROOT do
|
55
|
+
begin
|
56
|
+
chroot = params.delete("--root")
|
57
|
+
if globs = params.delete("--delete")
|
58
|
+
globs.split(",").each do |relative|
|
59
|
+
path = path_of(relative, chroot)
|
60
|
+
if path == root
|
61
|
+
raise BadRequest.new("Can't delete root")
|
62
|
+
end
|
63
|
+
rm_rf Dir[path]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
params.each do |relative, value|
|
68
|
+
path = path_of(relative, chroot)
|
69
|
+
if value.is_a?(Hash) && value[:filename]
|
70
|
+
# Uploading the file
|
71
|
+
mkdir_p(File.dirname(path))
|
72
|
+
if caching?
|
73
|
+
# Move it to the cache and symlink into place
|
74
|
+
mkdir_p cache
|
75
|
+
cached_file = File.join(cache, md5(value[:tempfile]))
|
76
|
+
mv value[:tempfile].path, cached_file
|
77
|
+
ln_sf cached_file, path
|
78
|
+
else
|
79
|
+
# Just dump it in the target
|
80
|
+
mv value[:tempfile].path, path
|
81
|
+
end
|
82
|
+
elsif !caching?
|
83
|
+
raise BadRequest # can't issue a cache request if not in that mode
|
84
|
+
else
|
85
|
+
# Symlink to the existing file in the cache
|
86
|
+
cached_file = File.join(cache, value)
|
87
|
+
mkdir_p(File.dirname(path))
|
88
|
+
ln_sf cached_file, path
|
89
|
+
end
|
90
|
+
end
|
91
|
+
"ok"
|
92
|
+
rescue BadRequest
|
93
|
+
400
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
post CACHED do
|
98
|
+
JSON.parse(request.body.read).select do |md5|
|
99
|
+
File.exists?(File.join(cache, md5))
|
100
|
+
end.to_json
|
101
|
+
end
|
102
|
+
|
103
|
+
post PASSWORD do
|
104
|
+
self.class.password = request.body.read
|
105
|
+
"ok"
|
106
|
+
end
|
107
|
+
|
108
|
+
not_found do
|
109
|
+
"not found"
|
110
|
+
end
|
111
|
+
|
112
|
+
error do
|
113
|
+
"error: #{request.env['sinatra.error'].message}"
|
114
|
+
end
|
115
|
+
|
116
|
+
def path_of(relative, chroot = nil)
|
117
|
+
new_root = case chroot
|
118
|
+
when nil; root
|
119
|
+
when /^\//; chroot
|
120
|
+
else; File.join(root, chroot)
|
121
|
+
end
|
122
|
+
new_path = File.expand_path(File.join(new_root, CGI::unescape(relative)))
|
123
|
+
unless new_path[0, root.length] == root
|
124
|
+
raise "Can't leave root"
|
125
|
+
end
|
126
|
+
new_path
|
127
|
+
end
|
128
|
+
|
129
|
+
def cache
|
130
|
+
File.join(root, ".cache")
|
131
|
+
end
|
132
|
+
|
133
|
+
def caching?
|
134
|
+
self.class.caching
|
135
|
+
end
|
136
|
+
|
137
|
+
def root
|
138
|
+
self.class.root
|
139
|
+
end
|
140
|
+
|
141
|
+
module ClassMethods
|
142
|
+
attr_reader :root
|
143
|
+
|
144
|
+
def caching
|
145
|
+
!!@caching
|
146
|
+
end
|
147
|
+
|
148
|
+
def root=(root)
|
149
|
+
@root = File.expand_path(root)
|
150
|
+
end
|
151
|
+
|
152
|
+
def caching=(caching)
|
153
|
+
@caching = caching
|
154
|
+
end
|
155
|
+
|
156
|
+
def authenticate(user, pwd)
|
157
|
+
# puts "#{pwd} == #{password}"
|
158
|
+
pwd == password
|
159
|
+
end
|
160
|
+
|
161
|
+
def password
|
162
|
+
@password ||= begin
|
163
|
+
tmp = File.exists?(password_file) ?
|
164
|
+
File.read(password_file) :
|
165
|
+
"default"
|
166
|
+
tmp
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def password=(pwd)
|
171
|
+
File.open(password_file, "w") {|f| f.print(pwd) }
|
172
|
+
@password = pwd
|
173
|
+
end
|
174
|
+
|
175
|
+
def password_file
|
176
|
+
File.join(root, ".password")
|
177
|
+
end
|
178
|
+
|
179
|
+
# Helpful for testing clients
|
180
|
+
def start_embedded!(root, password)
|
181
|
+
ppid = Process.pid
|
182
|
+
Filezor::Server.root = root
|
183
|
+
Filezor::Server.password = password
|
184
|
+
fork do
|
185
|
+
harikari(ppid)
|
186
|
+
STDOUT.reopen("/dev/null")
|
187
|
+
Filezor::Server.run!
|
188
|
+
end
|
189
|
+
|
190
|
+
sleep 1
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def harikari(ppid)
|
196
|
+
Thread.new do
|
197
|
+
loop do
|
198
|
+
begin
|
199
|
+
Process.kill(0, ppid)
|
200
|
+
rescue
|
201
|
+
exit
|
202
|
+
end
|
203
|
+
sleep 1
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
extend ClassMethods
|
209
|
+
|
210
|
+
end
|
data/lib/filezor/util.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "digest/md5"
|
2
|
+
require "stringio"
|
3
|
+
module Filezor::Util
|
4
|
+
BUFFER_SIZE = 2**16
|
5
|
+
|
6
|
+
# Streaming MD5 on IO objects
|
7
|
+
def md5(io)
|
8
|
+
md5 = Digest::MD5.new
|
9
|
+
while chunk = io.read(BUFFER_SIZE)
|
10
|
+
md5 << chunk
|
11
|
+
end
|
12
|
+
io.rewind
|
13
|
+
md5.hexdigest
|
14
|
+
end
|
15
|
+
|
16
|
+
def files_from_map(map)
|
17
|
+
map.inject([]) do |memo, (path, value)|
|
18
|
+
memo << Filezor::File.new(StringIO.new(value), path)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
extend self
|
23
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
fixtures = File.dirname(__FILE__) + "/../fixtures/."
|
4
|
+
@root = File.dirname(__FILE__) + "/../tmp/filezor-test"
|
5
|
+
rm_rf @root
|
6
|
+
mkdir_p @root
|
7
|
+
cp_r fixtures, @root
|
8
|
+
|
9
|
+
Filezor::Server.start_embedded!(@root, "hiworld")
|
10
|
+
|
11
|
+
describe Filezor::Client do
|
12
|
+
|
13
|
+
before do
|
14
|
+
@pass = "hiworld"
|
15
|
+
@root = File.dirname(__FILE__) + "/../tmp/filezor-test"
|
16
|
+
@client = Filezor::Client.new("localhost", 4567, @pass)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should be able to deal with a server on a subpath" do
|
20
|
+
@client = Filezor::Client.new("localhost", 4567, @pass, "app_root" => "/filer")
|
21
|
+
@client.should_receive(:_get).with("http://admin:hiworld@localhost:4567/filer/ping").and_return(mock(:code => 200))
|
22
|
+
@client.ping
|
23
|
+
end
|
24
|
+
|
25
|
+
context "caching on" do
|
26
|
+
before do
|
27
|
+
@client.set_options("caching" => true, "root" => @root)
|
28
|
+
@file = Filezor::File.new(fixture_file("a.txt"), "x/y.txt")
|
29
|
+
@client.put(@file)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should have an md5" do
|
33
|
+
@file.md5.should == "11e561e2cfcd0ac2b4bb0089a665b031"
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should say its cached" do
|
37
|
+
@client.cached(@file).should == ["11e561e2cfcd0ac2b4bb0089a665b031"]
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#sync" do
|
41
|
+
it "should send the md5, not the file" do
|
42
|
+
@file = Filezor::File.new(fixture_file("a.txt"), "b.txt")
|
43
|
+
@client.should_receive(:cached).and_return(["11e561e2cfcd0ac2b4bb0089a665b031"])
|
44
|
+
@client.should_receive(:post).with("", {"--delete" => "x", "x/b.txt" => "11e561e2cfcd0ac2b4bb0089a665b031" })
|
45
|
+
@client.sync("x", @file)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
[false, true].each do |caching_state|
|
51
|
+
context "Caching is #{caching_state.inspect}" do
|
52
|
+
before do
|
53
|
+
@client.set_options("caching" => caching_state, "root" => @root)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should receive options" do
|
57
|
+
@client.options.should be_a(Hash)
|
58
|
+
@client.options["caching"].should == caching_state
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should be able to ping" do
|
62
|
+
@client.ping.should be_true
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should get errors on bad password" do
|
66
|
+
proc do
|
67
|
+
@client = Filezor::Client.new("localhost", 4567, "wrongpassword")
|
68
|
+
@client.get("/anything")
|
69
|
+
end.should raise_error(Filezor::Client::Unauthorized)
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#sync" do
|
73
|
+
before do
|
74
|
+
@path = "a/b/c.txt"
|
75
|
+
mkdir_p "#{@root}/a/b"
|
76
|
+
@unwanted = "#{@root}/a/b/gone.txt"
|
77
|
+
File.open(@unwanted, "w"){|f| f.puts "hi" }
|
78
|
+
file = Filezor::File.new(fixture_file("a.txt"), "c.txt")
|
79
|
+
@client.sync("a/b", file)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should upload the files" do
|
83
|
+
File.exists?(File.join(@root, @path)).should be_true
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should clear out other files in the folder" do
|
87
|
+
File.exists?(@unwanted).should be_false
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#put" do
|
92
|
+
it "should only take Filezor::Files" do
|
93
|
+
proc do
|
94
|
+
@client.put("something")
|
95
|
+
end.should raise_error(Filezor::Client::InvalidArgument)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should upload the files" do
|
99
|
+
path = "a/b/c.txt"
|
100
|
+
file = Filezor::File.new(fixture_file("a.txt"), path)
|
101
|
+
@client.put(file)
|
102
|
+
File.exists?(File.join(@root, path)).should be_true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Filezor::Server do
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
def app
|
7
|
+
Filezor::Server
|
8
|
+
end
|
9
|
+
|
10
|
+
def login(user = "admin", pass = "hiworld")
|
11
|
+
basic_authorize user, pass
|
12
|
+
end
|
13
|
+
|
14
|
+
before do
|
15
|
+
fixtures = File.dirname(__FILE__) + "/../fixtures/."
|
16
|
+
@root = File.dirname(__FILE__) + "/../tmp/filezor-test"
|
17
|
+
rm_rf @root
|
18
|
+
mkdir_p @root
|
19
|
+
cp_r fixtures, @root
|
20
|
+
app.root = @root
|
21
|
+
app.password = "hiworld"
|
22
|
+
app.caching = false
|
23
|
+
end
|
24
|
+
|
25
|
+
context "without auth" do
|
26
|
+
it "should not work" do
|
27
|
+
get "/"
|
28
|
+
last_response.status.should == 401
|
29
|
+
post "/asdfasdf/fds/d/dd"
|
30
|
+
last_response.status.should == 401
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "with incorrect login" do
|
35
|
+
it "should complain" do
|
36
|
+
login("admin", "wrong")
|
37
|
+
get "/file/a.txt"
|
38
|
+
last_response.status.should == 401
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "with auth" do
|
43
|
+
before do
|
44
|
+
login
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should be able to get a file" do
|
48
|
+
get "/file/a.txt"
|
49
|
+
last_response.should be_ok
|
50
|
+
last_response.body.should == fixture_file("a.txt").read
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should be able to ping" do
|
54
|
+
get "/ping"
|
55
|
+
last_response.body.should == "ok"
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should 404 on file not found" do
|
59
|
+
get "/file/what.txt"
|
60
|
+
last_response.status.should == 404
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "posting files" do
|
64
|
+
before do
|
65
|
+
post "/file", "hi.txt" => upload_fixture_file("a.txt")
|
66
|
+
last_response.should be_ok
|
67
|
+
last_response.body.should == "ok"
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should be readable" do
|
71
|
+
get "/file/hi.txt"
|
72
|
+
last_response.body.should == fixture_file("a.txt").read
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should fail on cached posts" do
|
76
|
+
post "/file", "what.txt" => "11e561e2cfcd0ac2b4bb0089a665b031"
|
77
|
+
last_response.status.should == 400
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "posting files" do
|
82
|
+
before do
|
83
|
+
post "/file", "hi.txt" => upload_fixture_file("a.txt")
|
84
|
+
last_response.should be_ok
|
85
|
+
last_response.body.should == "ok"
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should be readable" do
|
89
|
+
get "/file/hi.txt"
|
90
|
+
last_response.body.should == fixture_file("a.txt").read
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should fail on cached posts" do
|
94
|
+
post "/file", "what.txt" => "11e561e2cfcd0ac2b4bb0089a665b031"
|
95
|
+
last_response.status.should == 400
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should be able to chroot deeper" do
|
100
|
+
post "/file", "--root" => "omg/i/m/deeper", "a.txt" => upload_fixture_file("a.txt")
|
101
|
+
get "/file/omg/i/m/deeper/a.txt"
|
102
|
+
last_response.status.should == 200
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should be able to delete with a post" do
|
106
|
+
post "/file", "a.txt" => upload_fixture_file("a.txt"), "b.txt" => upload_fixture_file("b.txt")
|
107
|
+
get "/file/b.txt"
|
108
|
+
last_response.status.should == 200
|
109
|
+
post "/file", "--delete" => "*", "a.txt" => upload_fixture_file("a.txt")
|
110
|
+
get "/file/b.txt"
|
111
|
+
last_response.status.should == 404
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should be able to delete with a post and chroot" do
|
115
|
+
post "/file", "--root" => "omg/i/m/deeper", "a.txt" => upload_fixture_file("a.txt"), "b.txt" => upload_fixture_file("b.txt")
|
116
|
+
get "/file/omg/i/m/deeper/b.txt"
|
117
|
+
last_response.status.should == 200
|
118
|
+
post "/file", "--root" => "omg/i/m/deeper", "--delete" => "*", "a.txt" => upload_fixture_file("a.txt")
|
119
|
+
get "/file/omg/i/m/deeper/b.txt"
|
120
|
+
last_response.status.should == 404
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
it "should be able to post a few files" do
|
125
|
+
post "/file", "a.txt" => upload_fixture_file("a.txt"), "b.txt" => upload_fixture_file("b.txt")
|
126
|
+
last_response.should be_ok
|
127
|
+
last_response.body.should == "ok"
|
128
|
+
get "/file/a.txt"
|
129
|
+
last_response.body.should == fixture_file("a.txt").read
|
130
|
+
get "/file/b.txt"
|
131
|
+
last_response.body.should == fixture_file("b.txt").read
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should be able to delete a file" do
|
135
|
+
get "/file/a.txt"
|
136
|
+
last_response.body.should == fixture_file("a.txt").read
|
137
|
+
delete "/file/a.txt"
|
138
|
+
last_response.body.should == "ok"
|
139
|
+
get "/file/a.txt"
|
140
|
+
last_response.status.should == 404
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should be able to set the password" do
|
144
|
+
post "/auth", "newpwd"
|
145
|
+
last_response.body.should == "ok"
|
146
|
+
|
147
|
+
get "/file/a.txt"
|
148
|
+
last_response.status.should == 401
|
149
|
+
|
150
|
+
login("admin", "newpwd")
|
151
|
+
|
152
|
+
get "/file/a.txt"
|
153
|
+
last_response.status.should == 200
|
154
|
+
|
155
|
+
post "/auth", @pass = "hiworld"
|
156
|
+
last_response.body.should == "ok"
|
157
|
+
end
|
158
|
+
|
159
|
+
it "should respond with info" do
|
160
|
+
get "/info"
|
161
|
+
JSON.parse(last_response.body)["caching"].should === false
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should be able to modify info" do
|
165
|
+
post "/info", {"caching" => true }.to_json
|
166
|
+
JSON.parse(last_response.body)["caching"].should === true
|
167
|
+
|
168
|
+
get "/info"
|
169
|
+
JSON.parse(last_response.body)["caching"].should === true
|
170
|
+
end
|
171
|
+
|
172
|
+
context "in caching mode" do
|
173
|
+
before do
|
174
|
+
app.caching = true
|
175
|
+
post "/file", "hi.txt" => upload_fixture_file("a.txt")
|
176
|
+
last_response.should be_ok, last_response.inspect
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should have cached the file" do
|
180
|
+
File.exists?(File.join(app.root, ".cache", "11e561e2cfcd0ac2b4bb0089a665b031")).should be_true
|
181
|
+
File.symlink?(File.join(app.root, "hi.txt")).should be_true
|
182
|
+
end
|
183
|
+
|
184
|
+
it "should be able to query the cache" do
|
185
|
+
post "/cached", %w[11e561e2cfcd0ac2b4bb0089a665b031 not_there].to_json
|
186
|
+
last_response.status.should == 200
|
187
|
+
last_response.body.should == ["11e561e2cfcd0ac2b4bb0089a665b031"].to_json
|
188
|
+
end
|
189
|
+
|
190
|
+
it "should be able to do extra symlinks" do
|
191
|
+
post "/file", "from_md5.txt" => "11e561e2cfcd0ac2b4bb0089a665b031"
|
192
|
+
target = File.join(app.root, "from_md5.txt")
|
193
|
+
File.symlink?(target).should be_true
|
194
|
+
File.read(target).should == fixture_file("a.txt").read
|
195
|
+
end
|
196
|
+
|
197
|
+
it "should be able to wipe out the old files" do
|
198
|
+
post "/file", "z/from_md5.txt" => "11e561e2cfcd0ac2b4bb0089a665b031", "--delete" => "/z"
|
199
|
+
last_response.status.should == 200
|
200
|
+
File.exists?(File.join(app.root, "z", "f.txt")).should be_false
|
201
|
+
File.exists?(File.join(app.root, "z", "from_md5.txt")).should be_true
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should not be able to delete the root" do
|
205
|
+
post "/file", "from_md5.txt" => "11e561e2cfcd0ac2b4bb0089a665b031", "--delete" => "/"
|
206
|
+
last_response.status.should == 400
|
207
|
+
File.exists?(File.join(app.root, "a.txt")).should be_true
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
require "filezor"
|
4
|
+
describe "Filezor::Util" do
|
5
|
+
include Rack::Test::Methods
|
6
|
+
|
7
|
+
describe "#md5" do
|
8
|
+
it "should md5 a stream" do
|
9
|
+
Filezor::Util.md5(StringIO.new("hello")).should == Digest::MD5.hexdigest("hello")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#files_from_map" do
|
14
|
+
it "should convert" do
|
15
|
+
map = { "foo/bar" => "hello"}
|
16
|
+
files = Filezor::Util.files_from_map(map)
|
17
|
+
files.size.should == 1
|
18
|
+
file = files.first
|
19
|
+
file.path.should == "foo/bar"
|
20
|
+
file.tempfile.read.should == "hello"
|
21
|
+
file.md5.should == Digest::MD5.hexdigest("hello")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
hi world
|
data/spec/fixtures/a.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
C'est un file
|
data/spec/fixtures/b.txt
ADDED
File without changes
|
File without changes
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'rubygems'
|
4
|
+
require 'filezor'
|
5
|
+
require 'spec'
|
6
|
+
require 'spec/autorun'
|
7
|
+
require 'rack/test'
|
8
|
+
require 'fileutils'
|
9
|
+
include FileUtils
|
10
|
+
|
11
|
+
Spec::Runner.configure do |config|
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
def fixture_file(name)
|
16
|
+
File.open(File.dirname(__FILE__) + "/fixtures/#{name}")
|
17
|
+
end
|
18
|
+
|
19
|
+
def upload_fixture_file(name)
|
20
|
+
Rack::Test::UploadedFile.new(fixture_file(name).path, "application/binary")
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: filezor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 3
|
8
|
+
- 2
|
9
|
+
version: 1.3.2
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Kyle Maxwell
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-09-06 00:00:00 -07:00
|
18
|
+
default_executable: filezor
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: trollop
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :runtime
|
31
|
+
version_requirements: *id001
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: rest-client
|
34
|
+
prerelease: false
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ~>
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
segments:
|
40
|
+
- 1
|
41
|
+
- 3
|
42
|
+
version: "1.3"
|
43
|
+
type: :runtime
|
44
|
+
version_requirements: *id002
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: rspec
|
47
|
+
prerelease: false
|
48
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
segments:
|
53
|
+
- 1
|
54
|
+
- 2
|
55
|
+
- 9
|
56
|
+
version: 1.2.9
|
57
|
+
type: :development
|
58
|
+
version_requirements: *id003
|
59
|
+
description: pure ruby file sync
|
60
|
+
email: kyle@kylemaxwell.com
|
61
|
+
executables:
|
62
|
+
- filezor
|
63
|
+
extensions: []
|
64
|
+
|
65
|
+
extra_rdoc_files:
|
66
|
+
- LICENSE
|
67
|
+
- README.rdoc
|
68
|
+
files:
|
69
|
+
- .document
|
70
|
+
- .gitignore
|
71
|
+
- LICENSE
|
72
|
+
- README.rdoc
|
73
|
+
- Rakefile
|
74
|
+
- VERSION
|
75
|
+
- bin/filezor
|
76
|
+
- config/filezor.ru
|
77
|
+
- config/filezor.yml
|
78
|
+
- filezor.gemspec
|
79
|
+
- lib/filezor.rb
|
80
|
+
- lib/filezor/client.rb
|
81
|
+
- lib/filezor/file.rb
|
82
|
+
- lib/filezor/server.rb
|
83
|
+
- lib/filezor/util.rb
|
84
|
+
- spec/filezor/client_spec.rb
|
85
|
+
- spec/filezor/server_spec.rb
|
86
|
+
- spec/filezor/util_spec.rb
|
87
|
+
- spec/fixtures/.password
|
88
|
+
- spec/fixtures/a.txt
|
89
|
+
- spec/fixtures/b.txt
|
90
|
+
- spec/fixtures/z/f.txt
|
91
|
+
- spec/spec.opts
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
has_rdoc: true
|
94
|
+
homepage: http://github.com/fizx/filezor
|
95
|
+
licenses: []
|
96
|
+
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options:
|
99
|
+
- --charset=UTF-8
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
version: "0"
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
version: "0"
|
116
|
+
requirements: []
|
117
|
+
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 1.3.6
|
120
|
+
signing_key:
|
121
|
+
specification_version: 3
|
122
|
+
summary: pure ruby file sync
|
123
|
+
test_files:
|
124
|
+
- spec/filezor/client_spec.rb
|
125
|
+
- spec/filezor/server_spec.rb
|
126
|
+
- spec/filezor/util_spec.rb
|
127
|
+
- spec/spec_helper.rb
|