raval 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :gemcutter
2
+
3
+ gemspec
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 James Healy
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.
@@ -0,0 +1,202 @@
1
+ # raval
2
+
3
+ An experimental FTP server framework built on top of Celluloid. Celluloid is a
4
+ concurrency library that uses threads, so you will get the best results by
5
+ using jruby or rubinius.
6
+
7
+ By providing a simple driver class that responds to a handful of methods you
8
+ can have a complete FTP server.
9
+
10
+ The library is extracted from real world situations where an FTP interface was
11
+ required to sit in front of a non-filesystem persistence layer.
12
+
13
+ Some sample use cases include persisting data to:
14
+
15
+ * an Amazon S3 bucket
16
+ * a relational database
17
+ * redis
18
+ * memory
19
+
20
+ The examples directory contains a demonstration of in memory persistence.
21
+
22
+ ## Installation
23
+
24
+ gem install raval
25
+
26
+ ## Usage
27
+
28
+ To boot an FTP server you will need to provide a driver that speaks to your
29
+ persistence layer - the required driver contract is listed below.
30
+
31
+ Once that's ready, boot a new server like so:
32
+
33
+ require 'raval'
34
+
35
+ class MyDriver
36
+ .. implementation here ..
37
+ end
38
+
39
+ Raval::App.start(:host => "127.0.0.1",
40
+ :port => 3000,
41
+ :driver => MyDriver)
42
+
43
+ To get started quickly, try out the example in the git repo like so:
44
+
45
+ ruby -Ilib examples/fake.rb
46
+
47
+ ### The Driver Contract
48
+
49
+ The driver MUST have the following methods. Each method MUST accept the listed
50
+ parameters and return an appropriate value:
51
+
52
+ authenticate(user, pass)
53
+ - boolean indicating if the provided details are valid
54
+
55
+ bytes(path)
56
+ - an integer with the number of bytes in the file or nil if the file
57
+ doesn't exist
58
+
59
+ modified_time(path)
60
+ - a Time-ish object indicating when the requested path was last modified, or
61
+ nil if the file doesn't exist
62
+
63
+ change_dir(path)
64
+ - a boolean indicating if the current user is permitted to change to the
65
+ requested path
66
+
67
+ dir_contents(path)
68
+ - an array of the contents of the requested path or nil if the dir
69
+ doesn't exist. Each entry in the array should meet the Entry contract
70
+ defined below.
71
+
72
+ delete_dir(path)
73
+ - a boolean indicating if the directory was successfully deleted
74
+
75
+ delete_file(path)
76
+ - a boolean indicating if path was successfully deleted
77
+
78
+ rename(from_path, to_path)
79
+ - a boolean indicating if from_path was successfully renamed to to_path
80
+
81
+ make_dir(path)
82
+ - a boolean indicating if path was successfully created as a new directory
83
+
84
+ get_file(path)
85
+ - nil if the user isn't permitted to access that path
86
+ - an IOish (File, StringIO, IO, etc) object with data to send back to the
87
+ client
88
+
89
+ The driver MUST have one of the following methods. Each method MUST accept the
90
+ listed parameters and return an appropriate value:
91
+
92
+ put_file(path, tmp_file_path)
93
+ - an integer indicating the number of bytes received or False if there
94
+ was an error
95
+
96
+ put_file_streamed(path, datasocket)
97
+ - an integer indicating the number of bytes received or False if there
98
+ was an error
99
+
100
+ ### The File Entry Contract
101
+
102
+ The Driver#dir_contents method must return an array of Entry objects that
103
+ describe each item in a directory. Each of these Entry objects MUST have
104
+ the following methods and return values. All methods (except name) can
105
+ return nil if you're happy for raval to use a reasonable default.
106
+
107
+ directory
108
+ - a boolean indicating if the entry is a directory
109
+
110
+ size
111
+ - the number of bytes in a file. 0 if the entry is a directory
112
+
113
+ permissions
114
+ - a string describing the current users permissions on the entry
115
+
116
+ owner
117
+ - the name of the user that owns the entry
118
+
119
+ group
120
+ - the name of the group that owns the entry
121
+
122
+ time
123
+ - a Time-ish object that indicates when the entry was last modified
124
+
125
+ name
126
+ - the name of the file or directory
127
+
128
+ ### Driver Options
129
+
130
+ Some drivers may need configuration options - for server addresses or usernames
131
+ or whatever. To do so, just write your driver constructor to accept an options
132
+ hash, and then use it like so:
133
+
134
+ require 'raval'
135
+
136
+ class MyDriver
137
+ def initialize(opts = {})
138
+
139
+ end
140
+
141
+ .. remaining implementation here ..
142
+ end
143
+
144
+ Raval::App.start(:host => "127.0.0.1",
145
+ :port => 3000,
146
+ :driver => MyDriver,
147
+ :driver_opts => {:foo => :bar})
148
+
149
+ You can see an example of driver options in examples/fog.rb.
150
+
151
+ ## Contributors
152
+
153
+ * James Healy <james@yob.id.au> [http://www.yob.id.au](http://www.yob.id.au)
154
+ * John Nunemaker <nunemaker@gmail.com>
155
+ * Elijah Miller <elijah.miller@gmail.com>
156
+
157
+ ## Warning
158
+
159
+ FTP is an incredibly insecure protocol. Be careful about forcing users to authenticate
160
+ with a username or password that are important.
161
+
162
+ ## License
163
+
164
+ This library is distributed under the terms of the MIT License. See the included file for
165
+ more detail.
166
+
167
+ ## Contributing
168
+
169
+ All suggestions and patches welcome, preferably via a git repository I can pull from.
170
+ If this library proves useful to you, please let me know.
171
+
172
+ ## Further Reading
173
+
174
+ There are a range of RFCs that together specify the FTP protocol. In chronological
175
+ order, the more useful ones are:
176
+
177
+ * [http://tools.ietf.org/rfc/rfc959.txt](http://tools.ietf.org/rfc/rfc959.txt)
178
+ * [http://tools.ietf.org/rfc/rfc1123.txt](http://tools.ietf.org/rfc/rfc1123.txt)
179
+ * [http://tools.ietf.org/rfc/rfc2228.txt](http://tools.ietf.org/rfc/rfc2228.txt)
180
+ * [http://tools.ietf.org/rfc/rfc2389.txt](http://tools.ietf.org/rfc/rfc2389.txt)
181
+ * [http://tools.ietf.org/rfc/rfc2428.txt](http://tools.ietf.org/rfc/rfc2428.txt)
182
+ * [http://tools.ietf.org/rfc/rfc3659.txt](http://tools.ietf.org/rfc/rfc3659.txt)
183
+ * [http://tools.ietf.org/rfc/rfc4217.txt](http://tools.ietf.org/rfc/rfc4217.txt)
184
+
185
+ For an english summary that's somewhat more legible than the RFCs, and provides
186
+ some commentary on what features are actually useful or relevant 24 years after
187
+ RFC959 was published:
188
+
189
+ * [http://cr.yp.to/ftp.html](http://cr.yp.to/ftp.html)
190
+
191
+ For a history lesson, check out Appendix III of RCF959. It lists the preceding
192
+ (obsolete) RFC documents that relate to file transfers, including the ye old
193
+ RFC114 from 1971, "A File Transfer Protocol"
194
+
195
+ For more information on Celluloid, a library that (among other things) simplifies
196
+ writing applications that use sockets, check out their website.
197
+
198
+ * [http://celluloid.io/](http://celluloid.io/)
199
+
200
+ This library is heavily based on [em-ftpd](https://github.com/yob/em-ftpd), an FTPd
201
+ framework with similar design goals within the EventMachine ecosystem. It worked well
202
+ enough, but you know, callbacks and event loops make me something something.
@@ -0,0 +1,110 @@
1
+ # coding: utf-8
2
+
3
+ # a super simple FTP server with hard coded auth details and only two files
4
+ # available for download.
5
+ #
6
+ # The try it out, run this:
7
+ #
8
+ # ruby -Ilib examples/fake.rb
9
+
10
+ require 'ostruct'
11
+ require 'stringio'
12
+ require 'raval'
13
+
14
+ class FakeFTPDriver
15
+ FILE_ONE = "This is the first file available for download.\n\nBy James"
16
+ FILE_TWO = "This is the file number two.\n\n2009-03-21"
17
+
18
+ def change_dir(path)
19
+ path == "/" || path == "/files"
20
+ end
21
+
22
+ def dir_contents(path)
23
+ case path
24
+ when "/" then
25
+ [ dir_item("files"), file_item("one.txt", FILE_ONE.bytesize) ]
26
+ when "/files" then
27
+ [ file_item("two.txt", FILE_TWO.bytesize) ]
28
+ else
29
+ []
30
+ end
31
+ end
32
+
33
+ def authenticate(user, pass)
34
+ user == "test" && pass == "1234"
35
+ end
36
+
37
+ def bytes(path)
38
+ case path
39
+ when "/one.txt" then FILE_ONE.bytesize
40
+ when "/files/two.txt" then FILE_TWO.bytesize
41
+ else
42
+ false
43
+ end
44
+ end
45
+
46
+ def modified_time(path)
47
+ case path
48
+ when "/one.txt" then Time.now
49
+ when "/files/two.txt" then Time.now - 10000
50
+ else
51
+ false
52
+ end
53
+ end
54
+
55
+ def get_file(path)
56
+ case path
57
+ when "/one.txt" then StringIO.new(FILE_ONE)
58
+ when "/files/two.txt" then StringIO.new(FILE_TWO)
59
+ else
60
+ false
61
+ end
62
+ end
63
+
64
+ def put_file(path, tmp_path)
65
+ File.size(tmp_path)
66
+ end
67
+
68
+ def delete_file(path)
69
+ false
70
+ end
71
+
72
+ def delete_dir(path)
73
+ false
74
+ end
75
+
76
+ def rename(from, to)
77
+ false
78
+ end
79
+
80
+ def make_dir(path)
81
+ false
82
+ end
83
+
84
+ private
85
+
86
+ def dir_item(name)
87
+ OpenStruct.new(:name => name,
88
+ :directory => true,
89
+ :size => 0,
90
+ :owner => "user",
91
+ :group => "group",
92
+ :time => Time.now,
93
+ :permissions => "r--r--r--")
94
+ end
95
+
96
+ def file_item(name, bytes)
97
+ OpenStruct.new(:name => name,
98
+ :directory => false,
99
+ :size => bytes,
100
+ :owner => "user",
101
+ :group => "group",
102
+ :time => Time.now,
103
+ :permissions => "r--r--r--")
104
+ end
105
+
106
+ end
107
+
108
+ Raval::App.start(:host => "127.0.0.1",
109
+ :port => 3000,
110
+ :driver => FakeFTPDriver)
@@ -0,0 +1,183 @@
1
+ # coding: utf-8
2
+
3
+ # A FTP proxy to a cloud storage provider. Uses the fog gem to interface
4
+ # with the storage APIs.
5
+ #
6
+ # Any user will be permitted to log in with any password, but they are
7
+ # sandboxed to their own files. In the real world you will probably want to
8
+ # use real passwords.
9
+ #
10
+ # The try it out:
11
+ #
12
+ # 1) create an empty bucket/container with your cloud storage provider
13
+ # 2) edit the driver_options hash at the bottom of the file
14
+ # 3) run this:
15
+ #
16
+ # ruby -Ilib examples/fog.rb
17
+
18
+ require 'ostruct'
19
+ require 'stringio'
20
+ require 'raval'
21
+ require 'fog'
22
+
23
+ # the raval gem handles all the FTP protocol details, this driver just acts as
24
+ # an intermediary between raval and the chosen persistence layer - in this case
25
+ # that's Fog.
26
+ class FogFTPDriver
27
+
28
+ def initialize(options = {})
29
+ @options = options
30
+ end
31
+
32
+ def change_dir(path)
33
+ prefix = scoped_path_with_trailing_slash(path)
34
+ files = directory.files.all(:prefix => prefix)
35
+ files.any?
36
+ end
37
+
38
+ def dir_contents(path)
39
+ prefix = scoped_path_with_trailing_slash(path)
40
+ directories = []
41
+ files = []
42
+ directory.files.all(:prefix => prefix).each do |file|
43
+ tail = file.key.sub(prefix,"")
44
+ depth = tail.split("/").size
45
+ basename = tail.split("/").first
46
+ if depth == 1 && basename != ".placeholder"
47
+ files << file_item(basename, file.content_length)
48
+ elsif depth > 1 && directories.none? { |d| d.name == basename }
49
+ directories << dir_item(basename)
50
+ end
51
+ end
52
+ (directories + files).sort_by(&:name)
53
+ end
54
+
55
+ def authenticate(user, pass)
56
+ @user = user
57
+ true
58
+ end
59
+
60
+ def bytes(path)
61
+ prefix = scoped_path(path)
62
+ file = directory.files.head(prefix)
63
+ file ? file.content_length : nil
64
+ end
65
+
66
+ def modified_time(path)
67
+ prefix = scoped_path(path)
68
+ file = directory.files.head(prefix)
69
+ file ? file.last_modified : nil
70
+ end
71
+
72
+ def get_file(path)
73
+ prefix = scoped_path(path)
74
+ file = directory.files.get(prefix)
75
+ file ? file.body : nil
76
+ end
77
+
78
+ def put_file(path, tmp_path)
79
+ key = scoped_path(path)
80
+ File.open(tmp_path, "rb") do |io|
81
+ directory.files.create(:key => key,
82
+ :body => io)
83
+ end
84
+ File.size(tmp_path)
85
+ end
86
+
87
+ def delete_file(path)
88
+ prefix = scoped_path(path)
89
+ file = directory.files.head(prefix)
90
+ file ? file.destroy : nil
91
+ end
92
+
93
+ def delete_dir(path)
94
+ prefix = scoped_path_with_trailing_slash(path)
95
+ files = directory.files.all(:prefix => prefix)
96
+
97
+ if files.size == 1 && files.first.key[-12,12] == ".placeholder"
98
+ file = directory.files.get("#{prefix}.placeholder")
99
+ file.destroy
100
+ else
101
+ false
102
+ end
103
+ end
104
+
105
+ def rename(from, to)
106
+ scoped_from = scoped_path(from)
107
+ scoped_to = scoped_path(to)
108
+ old_file = directory.files.head(scoped_from)
109
+ if old_file
110
+ storage.copy_object(directory.key,scoped_from,directory.key,scoped_to)
111
+ old_file.destroy
112
+ else
113
+ nil
114
+ end
115
+ end
116
+
117
+ def make_dir(path)
118
+ prefix = scoped_path_with_trailing_slash(path)
119
+ files = directory.files.all(:prefix => prefix)
120
+ if files.any?
121
+ false # the dir already exists
122
+ else
123
+ # upload a placeholder file so the directory "exists"
124
+ directory.files.create(:key => "#{prefix}.placeholder",
125
+ :body => ".placeholder")
126
+ true
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def storage
133
+ @storage ||= Fog::Storage.new(@options.reject { |k,_| k == :directory})
134
+ end
135
+
136
+ def directory
137
+ @directory ||= storage.directories.get(@options[:directory])
138
+ end
139
+
140
+ def dir_item(name)
141
+ OpenStruct.new(:name => name,
142
+ :directory => true,
143
+ :size => 0,
144
+ :owner => "user",
145
+ :group => "group",
146
+ :time => Time.now,
147
+ :permissions => "r--r--r--")
148
+ end
149
+
150
+ def file_item(name, bytes)
151
+ OpenStruct.new(:name => name,
152
+ :directory => false,
153
+ :size => bytes,
154
+ :owner => "user",
155
+ :group => "group",
156
+ :time => Time.now,
157
+ :permissions => "r--r--r--")
158
+ end
159
+
160
+ def scoped_path_with_trailing_slash(path)
161
+ path = scoped_path(path)
162
+ path += "/" if path[-1,1] != "/"
163
+ path == "/" ? nil : path
164
+ end
165
+
166
+ def scoped_path(path)
167
+ path = "" if path == "/"
168
+
169
+ File.join("/", @user, path)[1,1024]
170
+ end
171
+
172
+ end
173
+
174
+ driver_opts = {:rackspace_username => 'RACKSPACE_USER',
175
+ :rackspace_api_key => 'RACKSPACE_KEY',
176
+ :provider => 'Rackspace',
177
+ :directory => 'RACKSPACE_CONTAINER'}
178
+
179
+ Raval::App.start(:host => "127.0.0.1",
180
+ :port => 3000,
181
+ :driver => FogFTPDriver,
182
+ :driver_opts => driver_opts
183
+ )