Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(files) move serving files into a plugin #130

Merged
merged 7 commits into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ server:start(function (request, response)
end)
```

Or try the [included examples](example/README.md).

## Features

- Compatible with Linux, Mac and Windows systems
Expand Down Expand Up @@ -121,6 +123,25 @@ local server = Pegasus:new({
server:start()
```


* pegasus.plugins.files

```lua
local Pegasus = require 'pegasus'
local Files = require 'pegasus.plugins.files'

local server = Pegasus:new({
plugins = {
Files:new {
location = "./",
default = "index.html",
},
}
})

server:start()
```

* pegasus.plugins.tls

```lua
Expand Down
25 changes: 19 additions & 6 deletions example/app.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ package.path = './src/?.lua;./src/?/init.lua;' .. package.path
local Pegasus = require 'pegasus'
local Compress = require 'pegasus.plugins.compress'
local Downloads = require 'pegasus.plugins.downloads'
local Files = require 'pegasus.plugins.files'
-- local TLS = require 'pegasus.plugins.tls'

local server = Pegasus:new({
port = '9090',
location = '/example/root/',
plugins = {
-- TLS:new { -- the tls specific configuration
-- wrap = {
Expand All @@ -30,19 +30,32 @@ local server = Pegasus:new({
-- },

Downloads:new {
prefix = "downloads",
location = '/example/root/',
prefix = 'downloads',
stripPrefix = true,
},

Files:new {
location = '/example/root/',
},

Compress:new(),
}
})

server:start(function(req)
local data = req:post()
server:start(function(req, resp)
local stop = false
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this variable, you can simply return the values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I originally had. I introduced the stop variable for readability. It makes it clear what the returned result is supposed to be. If results are directly returned one has to mentally parse every return statement, whereas this reads easy as the intent is clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me know if you want me to change this anyway. No problem.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree that it's making it more readable, but I'm fine to keep it there as it's just a code example :)


local path = req:path()
if req:method() ~= "POST" or path ~= "/index.html" then
return stop
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return stop
return false

end

local data = req:post()
if data then
print(data['name'])
print(data['age'])
print("Name: ", data.name)
print("Age: ", data.age)
end
stop = not not resp:writeFile("./example/root" .. path)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
stop = not not resp:writeFile("./example/root" .. path)
return not not resp:writeFile("./example/root" .. path)

return stop
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return stop

end)
2 changes: 1 addition & 1 deletion example/copas.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ local Downloads = require 'pegasus.plugins.downloads'
-- @tparam[opt] table opts.sslparams the tls based parameters, see the Copas documentation.
-- If not provided, then the connection will be accepted as a plain one.
-- @tparam[opt] table opts.plugins the plugins to use
-- @tparam[opt] function opts.handler the callback function to handle requests
-- @tparam[opt] function opts.callback the callback function to handle requests
-- @tparam[opt] string opts.location the file-path from where to server files
-- @return the server-socket on success, or nil+err on failure
local function newPegasusServer(opts)
Expand Down
1 change: 1 addition & 0 deletions rockspecs/pegasus-dev-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ build = {
['pegasus.compress'] = 'src/pegasus/compress.lua',
['pegasus.plugins.compress'] = 'src/pegasus/plugins/compress.lua',
['pegasus.plugins.downloads'] = 'src/pegasus/plugins/downloads.lua',
['pegasus.plugins.files'] = 'src/pegasus/plugins/files.lua',
['pegasus.plugins.tls'] = 'src/pegasus/plugins/tls.lua',
}
}
2 changes: 1 addition & 1 deletion spec/integration/integration_spec.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
describe('integration', function()
local port = '7070'
local url = 'http://localhost:' .. port
local url = 'http://localhost:' .. port .. "/index.html"

local executeCommand = function(command)
local handle = assert(io.popen(command .. ' -s ' .. url))
Expand Down
127 changes: 127 additions & 0 deletions spec/unit/files_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
describe("Files plugin", function()

local Files = require "pegasus.plugins.files"



describe("instantiation", function()

local options = {}
local plugin = Files:new(options)

it("should return a table", function()
assert.is.table(plugin)
end)


it("should have a default location; '.'", function()
assert.is.equal(".", plugin.location)
end)


it("should have a default; '/index.html'", function()
assert.is.equal("/index.html", plugin.default)
end)

end)



describe("invocation", function()

local redirect_called, writeFile_called
local request = {}
local response = {
redirect = function(self, ...) redirect_called = {...} end,
writeFile = function(self, ...) writeFile_called = {...} return self end,
-- finish = function(self, ...) end,
-- setHeader = function(self, ...) end,
-- setStatusCode = function(self, ...) end,
}

before_each(function()
redirect_called = nil
writeFile_called = nil
end)


it("handles GET", function()
stub(request, "path", function() return "/some/file.html" end)
stub(request, "method", function() return "GET" end)
local stop = Files:new():newRequestResponse(request, response)
assert.is.True(stop)
assert.is.Nil(redirect_called)
assert.are.same({
"./some/file.html",
"text/html"
}, writeFile_called)
end)

it("handles HEAD", function()
stub(request, "path", function() return "/some/file.html" end)
stub(request, "method", function() return "HEAD" end)
local stop = Files:new():newRequestResponse(request, response)
assert.is.True(stop)
assert.is.Nil(redirect_called)
assert.are.same({
"./some/file.html",
"text/html"
}, writeFile_called)
end)

it("doesn't handle POST", function()
stub(request, "path", function() return "/some/file.html" end)
stub(request, "method", function() return "POST" end)
local stop = Files:new():newRequestResponse(request, response)
assert.is.False(stop)
assert.is.Nil(redirect_called)
assert.is.Nil(writeFile_called)
end)

it("doesn't handle PUT", function()
stub(request, "path", function() return "/some/file.html" end)
stub(request, "method", function() return "PUT" end)
local stop = Files:new():newRequestResponse(request, response)
assert.is.False(stop)
assert.is.Nil(redirect_called)
assert.is.Nil(writeFile_called)
end)

it("redirects GET /", function()
stub(request, "path", function() return "/" end)
stub(request, "method", function() return "GET" end)
local stop = Files:new():newRequestResponse(request, response)
assert.is.True(stop)
assert.are.same({
"/index.html"
}, redirect_called)
assert.is.Nil(writeFile_called)
end)

it("serves from specified location", function()
stub(request, "path", function() return "/some/file.html" end)
stub(request, "method", function() return "GET" end)
local stop = Files:new({ location = "./location" }):newRequestResponse(request, response)
assert.is.True(stop)
assert.is.Nil(redirect_called)
assert.are.same({
"./location/some/file.html",
"text/html"
}, writeFile_called)
end)

it("forces location to be relative", function()
stub(request, "path", function() return "/some/file.html" end)
stub(request, "method", function() return "GET" end)
local stop = Files:new({ location = "/location" }):newRequestResponse(request, response)
assert.is.True(stop)
assert.is.Nil(redirect_called)
assert.are.same({
"./location/some/file.html",
"text/html"
}, writeFile_called)
end)

end)

end)
59 changes: 19 additions & 40 deletions src/pegasus/handler.lua
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
local Request = require 'pegasus.request'
local Response = require 'pegasus.response'
local mimetypes = require 'mimetypes'
local lfs = require 'lfs'
local Files = require 'pegasus.plugins.files'

local Handler = {}
Handler.__index = Handler

function Handler:new(callback, location, plugins)
local handler = {}
handler.callback = callback
handler.location = location or ''
handler.plugins = plugins or {}

if location then
handler.plugins[#handler.plugins+1] = Files:new {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure if I agree that a plugin should be called into the handler logic. In my view, the handler logic should work just with stuff that is part of the Pegasus core. I'm curious to see how you envision it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

location = location,
default = "/index.html",
}
end

local result = setmetatable(handler, self)
result:pluginsAlterRequestResponseMetatable()

Expand Down Expand Up @@ -109,58 +114,32 @@ function Handler:processRequest(port, client, server)
return false
end

local request = Request:new(port, client, server)
if not request:method() then
local request = Request:new(port, client, server, self)
local response = request.response

local method = request:method()
if not method then
client:close()
return
end

local response = Response:new(client, self)
response.request = request
local stop = self:pluginsNewRequestResponse(request, response)

if stop then
return
end

if request:path() and self.location ~= '' then
local path = request:path()
if path == '/' or path == '' then
path = 'index.html'
end
local filename = '.' .. self.location .. path

if not lfs.attributes(filename) then
response:statusCode(404)
end

stop = self:pluginsProcessFile(request, response, filename)

if stop then
return
end

local file = io.open(filename, 'rb')

if file then
response:writeFile(file, mimetypes.guess(filename or '') or 'text/html')
stop = true
else
response:statusCode(404)
end
end

if self.callback and not stop then
if self.callback then
response:statusCode(200)
response.headers = {}
response:addHeader('Content-Type', 'text/html')

self.callback(request, response)
stop = self.callback(request, response)
if stop then
return
end
end

if response.status == 404 then
response:writeDefaultErrorMessage(404)
end
response:writeDefaultErrorMessage(404)
end


Expand Down
Loading
Loading