raval 0.0.1

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/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
+ )