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 +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/lib/rack/tail_file/version.rb +7 -0
- data/lib/rack/tail_file.rb +178 -0
- data/rack-tail_file.gemspec +27 -0
- data/spec/fixtures/test.txt +5 -0
- data/spec/lib/rack/tail_file_spec.rb +146 -0
- metadata +161 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
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,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,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
|