luna 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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/agents.md +205 -0
- data/bin/luna +10 -0
- data/examples/static/README.md +7 -0
- data/examples/static/index.html +6 -0
- data/lib/luna/command/serve.rb +80 -0
- data/lib/luna/command.rb +14 -0
- data/lib/luna/middleware/markdown.rb +77 -0
- data/lib/luna/middleware/static.rb +122 -0
- data/lib/luna/middleware/verbose.rb +36 -0
- data/lib/luna/server.rb +37 -0
- data/lib/luna/version.rb +8 -0
- data/lib/luna.rb +9 -0
- data/license.md +21 -0
- data/readme.md +52 -0
- data.tar.gz.sig +0 -0
- metadata +186 -0
- metadata.gz.sig +0 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2e347831d31d4b1db1333c5bfb7ff3399fa58a95c5f776dcd68b2e8e8bd65987
|
|
4
|
+
data.tar.gz: 345be967db13b5f0368e2e576be3603fb6817c0dd6fbc9dce34ead65148ead17
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c159538036c29eb07e69bedcc79fa882e6dd3117acf79db1a1971034586230a63335f0856ad6b1cdc082675d938c5c631f3c1770cbfae5b3fa87c1ef93df7471
|
|
7
|
+
data.tar.gz: 0444b5af72dbb9cf8845e3aecbcdc5d1eba3b97af656b23ca828362155cd8b683a300ffd10619cbda79c109b10988346770b4d3a8a393da814ea4cb078831952
|
checksums.yaml.gz.sig
ADDED
|
Binary file
|
data/agents.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Agent
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
This section provides links to documentation from installed packages. It is automatically generated and may be updated by running `bake agent:context:install`.
|
|
6
|
+
|
|
7
|
+
**Important:** Before performing any code, documentation, or analysis tasks, always read and apply the full content of any relevant documentation referenced in the following sections. These context files contain authoritative standards and best practices for documentation, code style, and project-specific workflows. **Do not proceed with any actions until you have read and incorporated the guidance from relevant context files.**
|
|
8
|
+
|
|
9
|
+
**Setup Instructions:** If the referenced files are not present or if dependencies have been updated, run `bake agent:context:install` to install the latest context files.
|
|
10
|
+
|
|
11
|
+
### agent-context
|
|
12
|
+
|
|
13
|
+
Install and manage context files from Ruby gems.
|
|
14
|
+
|
|
15
|
+
#### [Getting Started](.context/agent-context/getting-started.md)
|
|
16
|
+
|
|
17
|
+
This guide explains how to use `agent-context`, a tool for discovering and installing contextual information from Ruby gems to help AI agents.
|
|
18
|
+
|
|
19
|
+
### async
|
|
20
|
+
|
|
21
|
+
A concurrency framework for Ruby.
|
|
22
|
+
|
|
23
|
+
#### [Getting Started](.context/async/getting-started.md)
|
|
24
|
+
|
|
25
|
+
This guide shows how to add async to your project and run code asynchronously.
|
|
26
|
+
|
|
27
|
+
#### [Scheduler](.context/async/scheduler.md)
|
|
28
|
+
|
|
29
|
+
This guide gives an overview of how the scheduler is implemented.
|
|
30
|
+
|
|
31
|
+
#### [Tasks](.context/async/tasks.md)
|
|
32
|
+
|
|
33
|
+
This guide explains how asynchronous tasks work and how to use them.
|
|
34
|
+
|
|
35
|
+
#### [Best Practices](.context/async/best-practices.md)
|
|
36
|
+
|
|
37
|
+
This guide gives an overview of best practices for using Async.
|
|
38
|
+
|
|
39
|
+
#### [Debugging](.context/async/debugging.md)
|
|
40
|
+
|
|
41
|
+
This guide explains how to debug issues with programs that use Async.
|
|
42
|
+
|
|
43
|
+
#### [Thread safety](.context/async/thread-safety.md)
|
|
44
|
+
|
|
45
|
+
This guide explains thread safety in Ruby, focusing on fibers and threads, common pitfalls, and best practices to avoid problems like data corruption, race conditions, and deadlocks.
|
|
46
|
+
|
|
47
|
+
### async-http
|
|
48
|
+
|
|
49
|
+
A HTTP client and server library.
|
|
50
|
+
|
|
51
|
+
#### [Getting Started](.context/async-http/getting-started.md)
|
|
52
|
+
|
|
53
|
+
This guide explains how to get started with `Async::HTTP`.
|
|
54
|
+
|
|
55
|
+
#### [Testing](.context/async-http/testing.md)
|
|
56
|
+
|
|
57
|
+
This guide explains how to use `Async::HTTP` clients and servers in your tests.
|
|
58
|
+
|
|
59
|
+
### bake
|
|
60
|
+
|
|
61
|
+
A replacement for rake with a simpler syntax.
|
|
62
|
+
|
|
63
|
+
#### [Getting Started](.context/bake/getting-started.md)
|
|
64
|
+
|
|
65
|
+
This guide gives a general overview of `bake` and how to use it.
|
|
66
|
+
|
|
67
|
+
#### [Command Line Interface](.context/bake/command-line-interface.md)
|
|
68
|
+
|
|
69
|
+
The `bake` command is broken up into two main functions: `list` and `call`.
|
|
70
|
+
|
|
71
|
+
#### [Project Integration](.context/bake/project-integration.md)
|
|
72
|
+
|
|
73
|
+
This guide explains how to add `bake` to a Ruby project.
|
|
74
|
+
|
|
75
|
+
#### [Gem Integration](.context/bake/gem-integration.md)
|
|
76
|
+
|
|
77
|
+
This guide explains how to add `bake` to a Ruby gem and export standardised tasks for use by other gems and projects.
|
|
78
|
+
|
|
79
|
+
#### [Input and Output](.context/bake/input-and-output.md)
|
|
80
|
+
|
|
81
|
+
`bake` has built in tasks for reading input and writing output in different formats. While this can be useful for general processing, there are some limitations, notably that rich object representations like `json` and `yaml` often don't support stream processing.
|
|
82
|
+
|
|
83
|
+
### covered
|
|
84
|
+
|
|
85
|
+
A modern approach to code coverage.
|
|
86
|
+
|
|
87
|
+
#### [Getting Started](.context/covered/getting-started.md)
|
|
88
|
+
|
|
89
|
+
This guide explains how to get started with `covered` and integrate it with your test suite.
|
|
90
|
+
|
|
91
|
+
#### [Configuration](.context/covered/configuration.md)
|
|
92
|
+
|
|
93
|
+
This guide will help you to configure covered for your project's specific requirements.
|
|
94
|
+
|
|
95
|
+
### io-event
|
|
96
|
+
|
|
97
|
+
An event loop.
|
|
98
|
+
|
|
99
|
+
#### [Getting Started](.context/io-event/getting-started.md)
|
|
100
|
+
|
|
101
|
+
This guide explains how to use `io-event` for non-blocking IO.
|
|
102
|
+
|
|
103
|
+
### metrics
|
|
104
|
+
|
|
105
|
+
Application metrics and instrumentation.
|
|
106
|
+
|
|
107
|
+
#### [Getting Started](.context/metrics/getting-started.md)
|
|
108
|
+
|
|
109
|
+
This guide explains how to use `metrics` for capturing run-time metrics.
|
|
110
|
+
|
|
111
|
+
#### [Capture](.context/metrics/capture.md)
|
|
112
|
+
|
|
113
|
+
This guide explains how to use `metrics` for exporting metric definitions from your application.
|
|
114
|
+
|
|
115
|
+
#### [Testing](.context/metrics/testing.md)
|
|
116
|
+
|
|
117
|
+
This guide explains how to write assertions in your test suite to validate `metrics` are being emitted correctly.
|
|
118
|
+
|
|
119
|
+
### protocol-http
|
|
120
|
+
|
|
121
|
+
Provides abstractions to handle HTTP protocols.
|
|
122
|
+
|
|
123
|
+
#### [Getting Started](.context/protocol-http/getting-started.md)
|
|
124
|
+
|
|
125
|
+
This guide explains how to use `protocol-http` for building abstract HTTP interfaces.
|
|
126
|
+
|
|
127
|
+
#### [Message Body](.context/protocol-http/message-body.md)
|
|
128
|
+
|
|
129
|
+
This guide explains how to work with HTTP request and response message bodies using `Protocol::HTTP::Body` classes.
|
|
130
|
+
|
|
131
|
+
#### [Headers](.context/protocol-http/headers.md)
|
|
132
|
+
|
|
133
|
+
This guide explains how to work with HTTP headers using `protocol-http`.
|
|
134
|
+
|
|
135
|
+
#### [Middleware](.context/protocol-http/middleware.md)
|
|
136
|
+
|
|
137
|
+
This guide explains how to build and use HTTP middleware with `Protocol::HTTP::Middleware`.
|
|
138
|
+
|
|
139
|
+
#### [Streaming](.context/protocol-http/streaming.md)
|
|
140
|
+
|
|
141
|
+
This guide gives an overview of how to implement streaming requests and responses.
|
|
142
|
+
|
|
143
|
+
#### [Design Overview](.context/protocol-http/design-overview.md)
|
|
144
|
+
|
|
145
|
+
This guide explains the high level design of `protocol-http` in the context of wider design patterns that can be used to implement HTTP clients and servers.
|
|
146
|
+
|
|
147
|
+
### protocol-http1
|
|
148
|
+
|
|
149
|
+
A low level implementation of the HTTP/1 protocol.
|
|
150
|
+
|
|
151
|
+
#### [Getting Started](.context/protocol-http1/getting-started.md)
|
|
152
|
+
|
|
153
|
+
This guide explains how to get started with `protocol-http1`, a low-level implementation of the HTTP/1 protocol for building HTTP clients and servers.
|
|
154
|
+
|
|
155
|
+
### protocol-http2
|
|
156
|
+
|
|
157
|
+
A low level implementation of the HTTP/2 protocol.
|
|
158
|
+
|
|
159
|
+
#### [Getting Started](.context/protocol-http2/getting-started.md)
|
|
160
|
+
|
|
161
|
+
This guide explains how to use the `protocol-http2` gem to implement a basic HTTP/2 client.
|
|
162
|
+
|
|
163
|
+
### samovar
|
|
164
|
+
|
|
165
|
+
Samovar is a flexible option parser excellent support for sub-commands and help documentation.
|
|
166
|
+
|
|
167
|
+
#### [Getting Started](.context/samovar/getting-started.md)
|
|
168
|
+
|
|
169
|
+
This guide explains how to use `samovar` to build command-line tools and applications.
|
|
170
|
+
|
|
171
|
+
### sus
|
|
172
|
+
|
|
173
|
+
A fast and scalable test runner.
|
|
174
|
+
|
|
175
|
+
#### [Getting Started](.context/sus/getting-started.md)
|
|
176
|
+
|
|
177
|
+
This guide explains how to use the `sus` gem to write tests for your Ruby projects.
|
|
178
|
+
|
|
179
|
+
### sus-fixtures-async-http
|
|
180
|
+
|
|
181
|
+
Test fixtures for running in Async::HTTP.
|
|
182
|
+
|
|
183
|
+
#### [Getting Started](.context/sus-fixtures-async-http/getting-started.md)
|
|
184
|
+
|
|
185
|
+
This guide explains how to use the `sus-fixtures-async-http` gem to test HTTP clients and servers.
|
|
186
|
+
|
|
187
|
+
### traces
|
|
188
|
+
|
|
189
|
+
Application instrumentation and tracing.
|
|
190
|
+
|
|
191
|
+
#### [Getting Started](.context/traces/getting-started.md)
|
|
192
|
+
|
|
193
|
+
This guide explains how to use `traces` for tracing code execution.
|
|
194
|
+
|
|
195
|
+
#### [Context Propagation](.context/traces/context-propagation.md)
|
|
196
|
+
|
|
197
|
+
This guide explains how to propagate trace context between different execution contexts within your application using `Traces.current_context` and `Traces.with_context`.
|
|
198
|
+
|
|
199
|
+
#### [Testing](.context/traces/testing.md)
|
|
200
|
+
|
|
201
|
+
This guide explains how to test traces in your code.
|
|
202
|
+
|
|
203
|
+
#### [Capture](.context/traces/capture.md)
|
|
204
|
+
|
|
205
|
+
This guide explains how to use `traces` for exporting traces from your application. This can be used to document all possible traces.
|
data/bin/luna
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "samovar"
|
|
7
|
+
require "async"
|
|
8
|
+
require "async/http/endpoint"
|
|
9
|
+
|
|
10
|
+
require_relative "../server"
|
|
11
|
+
|
|
12
|
+
module Luna
|
|
13
|
+
module Command
|
|
14
|
+
class Serve < Samovar::Command
|
|
15
|
+
self.description = "Serve static files from a directory."
|
|
16
|
+
|
|
17
|
+
options do
|
|
18
|
+
option "-b/--bind <address>", "Bind address (e.g. http://localhost:3000)", default: "http://localhost:3000"
|
|
19
|
+
option "-p/--port <number>", "Override port", type: Integer
|
|
20
|
+
option "-h/--hostname <hostname>", "Override hostname"
|
|
21
|
+
option "-t/--timeout <seconds>", "I/O timeout", type: Float, default: nil
|
|
22
|
+
|
|
23
|
+
option "-r/--root <path>", "Root directory", default: nil
|
|
24
|
+
option "-i/--index <filename>", "Directory index", default: "index.html"
|
|
25
|
+
option "--[no]-directory-listing", "Enable directory listing", default: true
|
|
26
|
+
option "--[no]-markdown", "Enable Markdown rendering", default: true
|
|
27
|
+
option "-v/--[no]-verbose", "Verbose logging", default: false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Optional positional argument for root directory
|
|
31
|
+
one :path, "Root directory to serve", default: nil
|
|
32
|
+
|
|
33
|
+
def root_directory
|
|
34
|
+
# Use positional argument if provided, otherwise --root option, otherwise current directory
|
|
35
|
+
if @path
|
|
36
|
+
@path
|
|
37
|
+
elsif @options[:root]
|
|
38
|
+
@options[:root]
|
|
39
|
+
else
|
|
40
|
+
Dir.pwd
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def endpoint_options
|
|
45
|
+
@options.slice(:hostname, :port, :timeout)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def endpoint
|
|
49
|
+
Async::HTTP::Endpoint.parse(@options[:bind], **endpoint_options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def middleware
|
|
53
|
+
Luna::Server.middleware(
|
|
54
|
+
root: File.expand_path(root_directory),
|
|
55
|
+
verbose: @options[:verbose],
|
|
56
|
+
markdown: @options[:markdown],
|
|
57
|
+
index: @options[:index],
|
|
58
|
+
directory_listing: @options[:directory_listing]
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def call
|
|
63
|
+
Console.logger.info(self) do |buffer|
|
|
64
|
+
buffer.puts "Luna v#{Luna::VERSION} serving..."
|
|
65
|
+
buffer.puts "- Root: #{File.expand_path(root_directory)}"
|
|
66
|
+
buffer.puts "- Bind: #{endpoint}"
|
|
67
|
+
buffer.puts "- Markdown: #{@options[:markdown]} | Index: #{@options[:index]} | Directory Listing: #{@options[:directory_listing]}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
Async do |task|
|
|
71
|
+
server = Luna::Server.new(middleware, endpoint, protocol: endpoint.protocol)
|
|
72
|
+
|
|
73
|
+
server.run
|
|
74
|
+
|
|
75
|
+
task.children.each(&:wait)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/luna/command.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "command/serve"
|
|
7
|
+
|
|
8
|
+
module Luna
|
|
9
|
+
module Command
|
|
10
|
+
def self.call(*arguments)
|
|
11
|
+
Serve.call(*arguments)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "protocol/http/middleware"
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
require "markly"
|
|
10
|
+
rescue LoadError
|
|
11
|
+
# The gemspec declares dependency, but handle gracefully if not present at runtime.
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module Luna
|
|
15
|
+
module Middleware
|
|
16
|
+
# Render Markdown files to HTML on-the-fly.
|
|
17
|
+
# Intercepts requests that target .md/.markdown files.
|
|
18
|
+
class Markdown < Protocol::HTTP::Middleware
|
|
19
|
+
def initialize(app, root: Dir.pwd, index_candidates: ["index.md", "README.md"])
|
|
20
|
+
super(app)
|
|
21
|
+
@root = File.expand_path(root)
|
|
22
|
+
@index_candidates = index_candidates
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr :root
|
|
26
|
+
attr :index_candidates
|
|
27
|
+
|
|
28
|
+
def safe_join(path)
|
|
29
|
+
full = File.expand_path(File.join(@root, path))
|
|
30
|
+
return nil unless full.start_with?(@root)
|
|
31
|
+
full
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call(request)
|
|
35
|
+
return super unless request.method == "GET" || request.method == "HEAD"
|
|
36
|
+
path = request.path.split("?", 2).first
|
|
37
|
+
|
|
38
|
+
fs_path = safe_join(path)
|
|
39
|
+
return super unless fs_path
|
|
40
|
+
|
|
41
|
+
if File.directory?(fs_path)
|
|
42
|
+
candidate = @index_candidates.map {|n| File.join(fs_path, n)}.find {|p| File.file?(p)}
|
|
43
|
+
return render_file(candidate, head: request.method == "HEAD") if candidate
|
|
44
|
+
elsif File.file?(fs_path) && markdown_file?(fs_path)
|
|
45
|
+
return render_file(fs_path, head: request.method == "HEAD")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def markdown_file?(path)
|
|
54
|
+
[".md", ".markdown"].include?(File.extname(path).downcase)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render_html(markdown)
|
|
58
|
+
if defined?(::Markly)
|
|
59
|
+
::Markly.render_html(markdown)
|
|
60
|
+
else
|
|
61
|
+
# Fallback minimal rendering if markly isn't available:
|
|
62
|
+
escape = ->(s){s.to_s.gsub("&","&").gsub("<","<").gsub(">",">")}
|
|
63
|
+
"<pre>" + escape.call(markdown) + "</pre>"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_file(path, head: false)
|
|
68
|
+
content = File.read(path)
|
|
69
|
+
html = render_html(content)
|
|
70
|
+
headers = [
|
|
71
|
+
["content-type", "text/html; charset=utf-8"]
|
|
72
|
+
]
|
|
73
|
+
Protocol::HTTP::Response[200, headers, head ? nil : [html]]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "protocol/http/middleware"
|
|
7
|
+
require "protocol/http/body/file"
|
|
8
|
+
require "time"
|
|
9
|
+
|
|
10
|
+
module Luna
|
|
11
|
+
module Middleware
|
|
12
|
+
# Serve static files from a root directory, with optional index and directory listing.
|
|
13
|
+
class Static < Protocol::HTTP::Middleware
|
|
14
|
+
MIME_TYPES = {
|
|
15
|
+
".html" => "text/html; charset=utf-8",
|
|
16
|
+
".css" => "text/css",
|
|
17
|
+
".js" => "application/javascript",
|
|
18
|
+
".json" => "application/json",
|
|
19
|
+
".xml" => "application/xml",
|
|
20
|
+
".txt" => "text/plain; charset=utf-8",
|
|
21
|
+
".md" => "text/markdown; charset=utf-8",
|
|
22
|
+
".png" => "image/png",
|
|
23
|
+
".jpg" => "image/jpeg",
|
|
24
|
+
".jpeg" => "image/jpeg",
|
|
25
|
+
".gif" => "image/gif",
|
|
26
|
+
".svg" => "image/svg+xml",
|
|
27
|
+
".ico" => "image/x-icon",
|
|
28
|
+
".pdf" => "application/pdf",
|
|
29
|
+
".zip" => "application/zip",
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
def initialize(app, root: Dir.pwd, index: "index.html", directory_listing: true)
|
|
33
|
+
super(app)
|
|
34
|
+
@root = File.expand_path(root)
|
|
35
|
+
@index = index
|
|
36
|
+
@directory_listing = directory_listing
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr :root
|
|
40
|
+
attr :index
|
|
41
|
+
attr :directory_listing
|
|
42
|
+
|
|
43
|
+
def mime_type_for_extension(ext)
|
|
44
|
+
MIME_TYPES[ext.downcase] || "application/octet-stream"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def safe_join(path)
|
|
48
|
+
full = File.expand_path(File.join(@root, path))
|
|
49
|
+
return nil unless full.start_with?(@root)
|
|
50
|
+
full
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def call(request)
|
|
54
|
+
return super unless request.method == "GET" || request.method == "HEAD"
|
|
55
|
+
# Remove query and decode percent-encoding carefully:
|
|
56
|
+
request_path = request.path.split("?", 2).first
|
|
57
|
+
|
|
58
|
+
file_path = safe_join(request_path)
|
|
59
|
+
return super unless file_path
|
|
60
|
+
|
|
61
|
+
if File.exist?(file_path)
|
|
62
|
+
if File.directory?(file_path)
|
|
63
|
+
index_path = File.join(file_path, @index)
|
|
64
|
+
if File.file?(index_path)
|
|
65
|
+
return serve_file(index_path, head: request.method == "HEAD")
|
|
66
|
+
elsif @directory_listing
|
|
67
|
+
return serve_directory_listing(file_path, request_path)
|
|
68
|
+
else
|
|
69
|
+
return super
|
|
70
|
+
end
|
|
71
|
+
elsif File.file?(file_path)
|
|
72
|
+
return serve_file(file_path, head: request.method == "HEAD")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
super
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def serve_file(path, head: false)
|
|
82
|
+
stat = File.stat(path)
|
|
83
|
+
headers = [
|
|
84
|
+
["content-type", mime_type_for_extension(File.extname(path))],
|
|
85
|
+
["last-modified", stat.mtime.httpdate],
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
body = head ? nil : Protocol::HTTP::Body::File.open(path)
|
|
89
|
+
|
|
90
|
+
Protocol::HTTP::Response[200, headers, body]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def serve_directory_listing(dir_path, request_path)
|
|
94
|
+
entries = Dir.entries(dir_path).sort - ["."]
|
|
95
|
+
entries.reject! {|e| e == ".."} if request_path == "/"
|
|
96
|
+
links = entries.map do |e|
|
|
97
|
+
name = e
|
|
98
|
+
target = File.join(request_path, e)
|
|
99
|
+
target += "/" if File.directory?(File.join(dir_path, e)) && !target.end_with?("/")
|
|
100
|
+
"<li><a href=\"#{escape_html(target)}\">#{escape_html(name)}</a></li>"
|
|
101
|
+
end.join("\n")
|
|
102
|
+
html = <<~HTML
|
|
103
|
+
<!doctype html>
|
|
104
|
+
<meta charset="utf-8">
|
|
105
|
+
<title>Index of #{escape_html(request_path)}</title>
|
|
106
|
+
<h1>Index of #{escape_html(request_path)}</h1>
|
|
107
|
+
<ul>#{links}</ul>
|
|
108
|
+
HTML
|
|
109
|
+
|
|
110
|
+
headers = [
|
|
111
|
+
["content-type", "text/html; charset=utf-8"]
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
Protocol::HTTP::Response[200, headers, [html]]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def escape_html(s)
|
|
118
|
+
s.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "console"
|
|
7
|
+
require "async/http/statistics"
|
|
8
|
+
|
|
9
|
+
module Luna
|
|
10
|
+
module Middleware
|
|
11
|
+
# Simple HTTP logging middleware adapted from Falcon.
|
|
12
|
+
class Verbose < Protocol::HTTP::Middleware
|
|
13
|
+
def initialize(app, logger = Console)
|
|
14
|
+
super(app)
|
|
15
|
+
@logger = logger
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def annotate(request)
|
|
19
|
+
task = Async::Task.current
|
|
20
|
+
address = request.remote_address
|
|
21
|
+
@logger.info(request, "-> #{request.method} #{request.path}", headers: request.headers.to_h, address: address.inspect)
|
|
22
|
+
task.annotate("#{request.method} #{request.path} from #{address.inspect}")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(request)
|
|
26
|
+
annotate(request)
|
|
27
|
+
statistics = Async::HTTP::Statistics.start
|
|
28
|
+
response = super
|
|
29
|
+
statistics.wrap(response) do |body, error|
|
|
30
|
+
@logger.info(request, "<- #{request.method} #{request.path}", headers: response.headers.to_h, status: response.status, body: body.inspect, error: error)
|
|
31
|
+
end
|
|
32
|
+
response
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/luna/server.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "async/http/server"
|
|
7
|
+
require "protocol/http/middleware/builder"
|
|
8
|
+
require "protocol/http/content_encoding"
|
|
9
|
+
|
|
10
|
+
require_relative "middleware/verbose"
|
|
11
|
+
require_relative "middleware/static"
|
|
12
|
+
require_relative "middleware/markdown"
|
|
13
|
+
|
|
14
|
+
module Luna
|
|
15
|
+
# HTTP server hosting a middleware stack for static content.
|
|
16
|
+
class Server < Async::HTTP::Server
|
|
17
|
+
# Build a middleware for serving static files with optional markdown and logging.
|
|
18
|
+
# @param root [String] Root directory for content.
|
|
19
|
+
# @param verbose [Boolean] Enable verbose logging.
|
|
20
|
+
# @param markdown [Boolean] Enable markdown rendering.
|
|
21
|
+
# @param index [String] Index file for directories.
|
|
22
|
+
# @param directory_listing [Boolean] Enable basic directory listing when no index exists.
|
|
23
|
+
def self.middleware(root: Dir.pwd, verbose: false, markdown: true, index: "index.html", directory_listing: true)
|
|
24
|
+
::Protocol::HTTP::Middleware.build do
|
|
25
|
+
use Luna::Middleware::Verbose if verbose
|
|
26
|
+
use ::Protocol::HTTP::ContentEncoding
|
|
27
|
+
use Luna::Middleware::Markdown, root: root if markdown
|
|
28
|
+
run Luna::Middleware::Static.new(
|
|
29
|
+
->(request){Protocol::HTTP::Response[404, {"content-type"=>"text/plain"}, ["Not Found"]]},
|
|
30
|
+
root: root,
|
|
31
|
+
index: index,
|
|
32
|
+
directory_listing: directory_listing
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/luna/version.rb
ADDED
data/lib/luna.rb
ADDED
data/license.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright, 2025, by Samuel Williams.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/readme.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Luna
|
|
2
|
+
|
|
3
|
+
A tiny, asynchronous static file server with optional Markdown rendering, built on Protocol::HTTP and Async.
|
|
4
|
+
|
|
5
|
+
- Static files via `Luna::Middleware::Static`
|
|
6
|
+
- Optional Markdown-to-HTML via `Luna::Middleware::Markdown` (using `markly`)
|
|
7
|
+
- Simple verbose logging via `Luna::Middleware::Verbose`
|
|
8
|
+
|
|
9
|
+
[](https://github.com/socketry/luna/actions?workflow=Test)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add this line to your application's Gemfile:
|
|
14
|
+
|
|
15
|
+
gem "luna"
|
|
16
|
+
|
|
17
|
+
And then execute:
|
|
18
|
+
|
|
19
|
+
bundle install
|
|
20
|
+
|
|
21
|
+
Or install it yourself as:
|
|
22
|
+
|
|
23
|
+
gem install luna
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
Serve the current directory:
|
|
28
|
+
|
|
29
|
+
luna --bind http://localhost:3000
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
|
|
33
|
+
- `--root PATH` (default: current directory)
|
|
34
|
+
- `--index FILENAME` (default: `index.html`)
|
|
35
|
+
- `--[no]-directory-listing` (default: `true`)
|
|
36
|
+
- `--[no]-markdown` (default: `true`)
|
|
37
|
+
- `--[no]-verbose`
|
|
38
|
+
- `--bind URL` (default: `http://localhost:3000`)
|
|
39
|
+
|
|
40
|
+
## Programmatic Usage
|
|
41
|
+
|
|
42
|
+
require "async"
|
|
43
|
+
require "async/http/endpoint"
|
|
44
|
+
require "luna/server"
|
|
45
|
+
|
|
46
|
+
endpoint = Async::HTTP::Endpoint.parse("http://localhost:3000")
|
|
47
|
+
app = Luna::Server.middleware(root: "/path/to/root", markdown: true)
|
|
48
|
+
|
|
49
|
+
Async do |task|
|
|
50
|
+
server = Luna::Server.new(app, endpoint, protocol: endpoint.protocol)
|
|
51
|
+
server.run
|
|
52
|
+
end
|
data.tar.gz.sig
ADDED
|
Binary file
|
metadata
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: luna
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Samuel Williams
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain:
|
|
10
|
+
- |
|
|
11
|
+
-----BEGIN CERTIFICATE-----
|
|
12
|
+
MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
|
|
13
|
+
ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
|
|
14
|
+
CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
|
|
15
|
+
MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
|
|
16
|
+
MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
|
|
17
|
+
bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
|
|
18
|
+
igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
|
|
19
|
+
9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
|
|
20
|
+
sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
|
|
21
|
+
e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
|
|
22
|
+
XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
|
|
23
|
+
RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
|
|
24
|
+
tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
|
|
25
|
+
zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
|
|
26
|
+
xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
|
|
27
|
+
BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
|
|
28
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
|
|
29
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
|
|
30
|
+
cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
|
|
31
|
+
xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
|
|
32
|
+
c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
|
|
33
|
+
8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
|
|
34
|
+
JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
|
|
35
|
+
eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
|
|
36
|
+
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
|
37
|
+
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
|
38
|
+
-----END CERTIFICATE-----
|
|
39
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
40
|
+
dependencies:
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: async
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: async-http
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0.75'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0.75'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: bundler
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: console
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.30'
|
|
90
|
+
type: :runtime
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.30'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: markly
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0.6'
|
|
104
|
+
- - "<"
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '2.0'
|
|
107
|
+
type: :runtime
|
|
108
|
+
prerelease: false
|
|
109
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0.6'
|
|
114
|
+
- - "<"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '2.0'
|
|
117
|
+
- !ruby/object:Gem::Dependency
|
|
118
|
+
name: protocol-http
|
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0.31'
|
|
124
|
+
type: :runtime
|
|
125
|
+
prerelease: false
|
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0.31'
|
|
131
|
+
- !ruby/object:Gem::Dependency
|
|
132
|
+
name: samovar
|
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '2.3'
|
|
138
|
+
type: :runtime
|
|
139
|
+
prerelease: false
|
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '2.3'
|
|
145
|
+
executables:
|
|
146
|
+
- luna
|
|
147
|
+
extensions: []
|
|
148
|
+
extra_rdoc_files: []
|
|
149
|
+
files:
|
|
150
|
+
- agents.md
|
|
151
|
+
- bin/luna
|
|
152
|
+
- examples/static/README.md
|
|
153
|
+
- examples/static/index.html
|
|
154
|
+
- lib/luna.rb
|
|
155
|
+
- lib/luna/command.rb
|
|
156
|
+
- lib/luna/command/serve.rb
|
|
157
|
+
- lib/luna/middleware/markdown.rb
|
|
158
|
+
- lib/luna/middleware/static.rb
|
|
159
|
+
- lib/luna/middleware/verbose.rb
|
|
160
|
+
- lib/luna/server.rb
|
|
161
|
+
- lib/luna/version.rb
|
|
162
|
+
- license.md
|
|
163
|
+
- readme.md
|
|
164
|
+
homepage: https://github.com/socketry/luna
|
|
165
|
+
licenses:
|
|
166
|
+
- MIT
|
|
167
|
+
metadata:
|
|
168
|
+
source_code_uri: https://github.com/socketry/luna.git
|
|
169
|
+
rdoc_options: []
|
|
170
|
+
require_paths:
|
|
171
|
+
- lib
|
|
172
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
173
|
+
requirements:
|
|
174
|
+
- - ">="
|
|
175
|
+
- !ruby/object:Gem::Version
|
|
176
|
+
version: '3.2'
|
|
177
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
178
|
+
requirements:
|
|
179
|
+
- - ">="
|
|
180
|
+
- !ruby/object:Gem::Version
|
|
181
|
+
version: '0'
|
|
182
|
+
requirements: []
|
|
183
|
+
rubygems_version: 4.0.10
|
|
184
|
+
specification_version: 4
|
|
185
|
+
summary: A tiny, asynchronous static file server with optional Markdown rendering.
|
|
186
|
+
test_files: []
|
metadata.gz.sig
ADDED
|
Binary file
|