rack-tail_file 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/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rack-tail_file.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Beth
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Rack::TailFile
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'rack-tail_file'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install rack-tail_file
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task :default => :spec
@@ -0,0 +1,7 @@
1
+ module Rack
2
+
3
+ class TailFile
4
+ VERSION = "0.0.1"
5
+ end
6
+
7
+ end
@@ -0,0 +1,178 @@
1
+ require "rack/tail_file/version"
2
+
3
+ require 'elif'
4
+ require 'time'
5
+ require 'rack/utils'
6
+ require 'rack/mime'
7
+
8
+ module Rack
9
+ # Rack::File serves files below the +root+ directory given, according to the
10
+ # path info of the Rack request.
11
+ # e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file
12
+ # as http://localhost:9292/passwd
13
+ #
14
+ # Handlers can detect if bodies are a Rack::File, and use mechanisms
15
+ # like sendfile on the +path+.
16
+
17
+ class TailFile
18
+
19
+ SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
20
+ ALLOWED_VERBS = %w[GET HEAD]
21
+
22
+ attr_accessor :root
23
+ attr_accessor :path
24
+ attr_accessor :cache_control
25
+
26
+ alias :to_path :path
27
+
28
+ def initialize(root, headers={}, default_mime = 'text/plain')
29
+ @root = root
30
+ @headers = headers
31
+ @default_mime = default_mime
32
+ end
33
+
34
+ def call(env)
35
+ dup._call(env)
36
+ end
37
+
38
+ F = ::File
39
+
40
+ def _call(env)
41
+ return fail(405, "Method Not Allowed") unless method_allowed?(env)
42
+ return fail(403, "Forbidden") unless path_is_within_root?(env)
43
+
44
+ @path = file_path(env)
45
+
46
+ if available?
47
+ serving(env)
48
+ else
49
+ fail(404, "File not found: #{path_info_for(env)}")
50
+ end
51
+ end
52
+
53
+ def method_allowed? env
54
+ ALLOWED_VERBS.include? env["REQUEST_METHOD"]
55
+ end
56
+
57
+ def available?
58
+ begin
59
+ F.file?(@path) && F.readable?(@path)
60
+ rescue SystemCallError
61
+ false
62
+ end
63
+ end
64
+
65
+ def path_info_for env
66
+ Utils.unescape(env["PATH_INFO"])
67
+ end
68
+
69
+ def path_is_within_root? env
70
+ root = root env
71
+ target = target_file env
72
+ !target.relative_path_from(root).to_s.split(SEPS).any?{|p| p == ".."}
73
+ end
74
+
75
+ def target_file env
76
+ path_info = Pathname.new("").join(*path_info_for(env).split(SEPS))
77
+ root = root env
78
+ root.join(path_info)
79
+ end
80
+
81
+ def root env
82
+ Pathname.new(@root)
83
+ end
84
+
85
+ def file_path(env)
86
+ target_file env
87
+ end
88
+
89
+ def serving(env)
90
+ last_modified = F.mtime(@path).httpdate
91
+ return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
92
+
93
+ headers = { "Last-Modified" => last_modified }
94
+ mime = Mime.mime_type(F.extname(@path), @default_mime)
95
+ headers["Content-Type"] = mime if mime
96
+
97
+ # Set custom headers
98
+ @headers.each { |field, content| headers[field] = content } if @headers
99
+
100
+ response = [ 200, headers, env["REQUEST_METHOD"] == "HEAD" ? [] : self ]
101
+ response[1]["Content-Length"] = requested_size(env, response).to_s
102
+ response
103
+ end
104
+
105
+ def requested_lines_size(env)
106
+ (env.fetch("QUERY_STRING")[/\d+/] || 50).to_i
107
+ end
108
+
109
+ def tail_size_for line_count
110
+ elif = Elif.new(@path)
111
+ tail_size = 0
112
+ line_count.times do
113
+ begin
114
+ tail_size += Rack::Utils.bytesize(elif.readline)
115
+ rescue EOFError
116
+ return tail_size
117
+ end
118
+ end
119
+ tail_size - 1 # Don't include the first \n
120
+ end
121
+
122
+
123
+ def requested_size(env, response)
124
+ # NOTE:
125
+ # We check via File::size? whether this file provides size info
126
+ # via stat (e.g. /proc files often don't), otherwise we have to
127
+ # figure it out by reading the whole file into memory.
128
+ size = F.size?(@path) || Utils.bytesize(F.read(@path))
129
+
130
+ #TODO handle invalid lines
131
+ tail_size = tail_size_for requested_lines_size(env)
132
+
133
+ if tail_size == size
134
+ response[0] = 200
135
+ @range = 0..size-1
136
+ else
137
+ start_byte = size - tail_size - 1
138
+ @range = start_byte..size-1
139
+ response[0] = 206
140
+ response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
141
+ size = @range.end - @range.begin + 1
142
+ end
143
+
144
+ size
145
+ end
146
+
147
+ def each
148
+ F.open(@path, "rb") do |file|
149
+ file.seek(@range.begin)
150
+ remaining_len = @range.end-@range.begin+1
151
+ while remaining_len > 0
152
+ part = file.read([8192, remaining_len].min)
153
+ break unless part
154
+ remaining_len -= part.length
155
+
156
+ yield part
157
+ end
158
+ end
159
+ end
160
+
161
+ private
162
+
163
+ def fail(status, body)
164
+ body += "\n"
165
+ [
166
+ status,
167
+ {
168
+ "Content-Type" => "text/plain",
169
+ "Content-Length" => body.size.to_s,
170
+ "X-Cascade" => "pass"
171
+ },
172
+ [body]
173
+ ]
174
+ end
175
+
176
+ end
177
+ end
178
+
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rack/tail_file/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rack-tail_file"
8
+ spec.version = Rack::TailFile::VERSION
9
+ spec.authors = ["Beth"]
10
+ spec.email = ["beth@bethesque.com"]
11
+ spec.description = %q{Like Rack::File, but it serves the last lines of a file}
12
+ spec.summary = %q{A rack app that serves the last lines of a file}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "elif"
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "rack-test"
25
+ spec.add_development_dependency "rspec", "~> 2.14"
26
+ spec.add_development_dependency "pry"
27
+ end
@@ -0,0 +1,5 @@
1
+ 1
2
+ 2
3
+ 3
4
+ 4
5
+ 5
@@ -0,0 +1,146 @@
1
+ require 'rack/tail_file'
2
+ require 'rack/test'
3
+ describe Rack::TailFile do
4
+
5
+ include Rack::Test::Methods
6
+
7
+ def app
8
+ Rack::TailFile.new root
9
+ end
10
+
11
+ let(:root) { './spec/fixtures' }
12
+ let(:file_path) { root + file_name }
13
+ let(:file_name) { "/test.txt" }
14
+ let(:file_contents) { File.read(file_path )}
15
+
16
+ describe "GET" do
17
+
18
+ context "when the number of lines required is not specified" do
19
+
20
+ subject { get file_name }
21
+
22
+ it "returns the entire file" do
23
+ subject
24
+ expect(last_response.body).to eq(file_contents)
25
+ end
26
+
27
+ it "sets a status of 200" do
28
+ subject
29
+ expect(last_response.status).to eq 200
30
+ end
31
+ end
32
+
33
+ context "when a number of lines is specified" do
34
+ let(:file_contents) { File.readlines(file_path).last(2).join }
35
+
36
+ subject { get file_name, "lines" => "2" }
37
+
38
+ it "returns the last lines" do
39
+ subject
40
+ expect(last_response.body).to eq(file_contents)
41
+ end
42
+
43
+ it "sets a status of 206" do
44
+ subject
45
+ expect(last_response.status).to eq 206
46
+ end
47
+ end
48
+
49
+ context "when a number of lines is specified that is more than the number of lines in the file" do
50
+
51
+ subject { get file_name, "lines" => "50" }
52
+ let(:file_contents) { File.read(file_path )}
53
+
54
+ it "returns the entire file" do
55
+ subject
56
+ expect(last_response.body).to eq(file_contents)
57
+ end
58
+
59
+ it "sets a status of 200" do
60
+ subject
61
+ expect(last_response.status).to eq 200
62
+ end
63
+ end
64
+
65
+ context "when the file does not exist" do
66
+ subject { get "something.txt" }
67
+
68
+ it "sets a status of 404" do
69
+ subject
70
+ expect(last_response.status).to eq 404
71
+ end
72
+ end
73
+
74
+ context "when the file requested is outside the root" do
75
+ subject { get "../lib/rack/tail_file_spec.rb" }
76
+
77
+ it "returns a 403 Forbidden" do
78
+ subject
79
+ expect(last_response.status).to eq 403
80
+ end
81
+
82
+ it "does not return the file" do
83
+ subject
84
+ expect(last_response.body).to_not eq(file_contents)
85
+ end
86
+ end
87
+
88
+ context "when the file requested has a path with .. that resolves to a file within the root" do
89
+
90
+ subject { get "../fixtures/blah/..#{file_name}" }
91
+
92
+ it "returns a 200 Success" do
93
+ subject
94
+ expect(last_response.status).to eq 200
95
+ end
96
+ end
97
+ end
98
+
99
+ describe "HEAD" do
100
+
101
+ subject { head file_name }
102
+
103
+ context "when the number of lines required is not specified" do
104
+ let(:file_contents) { File.read(file_path )}
105
+
106
+ it "returns an empty body" do
107
+ subject
108
+ expect(last_response.body.size).to eq 0
109
+ end
110
+
111
+ it "sets a status of 200" do
112
+ subject
113
+ expect(last_response.status).to eq 200
114
+ end
115
+ end
116
+
117
+ context "when a number of lines is specified" do
118
+ let(:file_contents) { File.readlines(file_path).last(2).join }
119
+
120
+ subject { head file_name, "lines" => "2" }
121
+
122
+ it "returns an empty body" do
123
+ subject
124
+ expect(last_response.body.size).to eq 0
125
+ end
126
+
127
+ xit "sets a status of 206" do
128
+ subject
129
+ expect(last_response.status).to eq 206
130
+ end
131
+ end
132
+
133
+ end
134
+
135
+ %w{PUT POST DELETE PATCH}.each do | http_method |
136
+
137
+ describe http_method do
138
+ it "returns a 405 Method Not Allowed response" do
139
+ self.send(http_method.downcase.to_sym, "something")
140
+ expect(last_response.status).to eq 405
141
+ end
142
+ end
143
+
144
+ end
145
+
146
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-tail_file
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Beth
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-04-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: elif
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: bundler
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.3'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rack-test
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '2.14'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '2.14'
94
+ - !ruby/object:Gem::Dependency
95
+ name: pry
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: Like Rack::File, but it serves the last lines of a file
111
+ email:
112
+ - beth@bethesque.com
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - .gitignore
118
+ - .rspec
119
+ - Gemfile
120
+ - LICENSE.txt
121
+ - README.md
122
+ - Rakefile
123
+ - lib/rack/tail_file.rb
124
+ - lib/rack/tail_file/version.rb
125
+ - rack-tail_file.gemspec
126
+ - spec/fixtures/test.txt
127
+ - spec/lib/rack/tail_file_spec.rb
128
+ homepage: ''
129
+ licenses:
130
+ - MIT
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ none: false
137
+ requirements:
138
+ - - ! '>='
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ segments:
142
+ - 0
143
+ hash: 2187452435069552088
144
+ required_rubygems_version: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ segments:
151
+ - 0
152
+ hash: 2187452435069552088
153
+ requirements: []
154
+ rubyforge_project:
155
+ rubygems_version: 1.8.23
156
+ signing_key:
157
+ specification_version: 3
158
+ summary: A rack app that serves the last lines of a file
159
+ test_files:
160
+ - spec/fixtures/test.txt
161
+ - spec/lib/rack/tail_file_spec.rb