staticd 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
- data/bin/staticd +7 -0
- data/lib/rack/auth/hmac.rb +81 -0
- data/lib/rack/request_time.rb +19 -0
- data/lib/staticd.rb +7 -0
- data/lib/staticd/api.rb +356 -0
- data/lib/staticd/app.rb +130 -0
- data/lib/staticd/cache_engine.rb +45 -0
- data/lib/staticd/cli.rb +70 -0
- data/lib/staticd/config.rb +115 -0
- data/lib/staticd/database.rb +41 -0
- data/lib/staticd/datastore.rb +64 -0
- data/lib/staticd/datastores/local.rb +48 -0
- data/lib/staticd/datastores/s3.rb +63 -0
- data/lib/staticd/domain_generator.rb +42 -0
- data/lib/staticd/http_cache.rb +65 -0
- data/lib/staticd/http_server.rb +197 -0
- data/lib/staticd/json_request.rb +15 -0
- data/lib/staticd/json_response.rb +29 -0
- data/lib/staticd/models/base.rb +43 -0
- data/lib/staticd/models/domain_name.rb +17 -0
- data/lib/staticd/models/release.rb +20 -0
- data/lib/staticd/models/resource.rb +19 -0
- data/lib/staticd/models/route.rb +19 -0
- data/lib/staticd/models/site.rb +36 -0
- data/lib/staticd/models/staticd_config.rb +71 -0
- data/lib/staticd/public/jquery-1.11.1.min.js +4 -0
- data/lib/staticd/public/main.css +61 -0
- data/lib/staticd/public/main.js +15 -0
- data/lib/staticd/version.rb +3 -0
- data/lib/staticd/views/main.haml +9 -0
- data/lib/staticd/views/setup.haml +47 -0
- data/lib/staticd/views/welcome.haml +92 -0
- data/lib/staticd_utils/archive.rb +80 -0
- data/lib/staticd_utils/file_size.rb +26 -0
- data/lib/staticd_utils/gli_object.rb +14 -0
- data/lib/staticd_utils/memory_file.rb +36 -0
- data/lib/staticd_utils/sitemap.rb +100 -0
- data/lib/staticd_utils/tar.rb +43 -0
- metadata +300 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
require "digest/sha1"
|
2
|
+
|
3
|
+
module Staticd
|
4
|
+
module Datastores
|
5
|
+
|
6
|
+
# Datastore storing files on local directory.
|
7
|
+
#
|
8
|
+
# It use the file SHA1 digest as a filename so two identical files are not
|
9
|
+
# stored twice.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
# datastore = Staticd::Datastores::Local.new(path: "/tmp/datastore")
|
13
|
+
# datastore.put(file_path) unless datastore.exist?(file_path)
|
14
|
+
# # => "/tmp/datastore/sha1_digest"
|
15
|
+
class Local
|
16
|
+
|
17
|
+
def initialize(params)
|
18
|
+
@path = params[:path]
|
19
|
+
check_store_directory
|
20
|
+
end
|
21
|
+
|
22
|
+
def put(file_path)
|
23
|
+
datastore_file = stored_file_path(file_path)
|
24
|
+
FileUtils.copy_file(file_path, datastore_file) unless exist?(file_path)
|
25
|
+
datastore_file
|
26
|
+
end
|
27
|
+
|
28
|
+
def exist?(file_path)
|
29
|
+
datastore_file = stored_file_path(file_path)
|
30
|
+
File.exist?(datastore_file) ? datastore_file : false
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def check_store_directory
|
36
|
+
FileUtils.mkdir_p(@path) unless File.directory?(@path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def sha1(file_path)
|
40
|
+
Digest::SHA1.hexdigest(File.read(file_path))
|
41
|
+
end
|
42
|
+
|
43
|
+
def stored_file_path(file_path)
|
44
|
+
"#{@path}/#{sha1(file_path)}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "digest/sha1"
|
2
|
+
require "aws-sdk"
|
3
|
+
|
4
|
+
module Staticd
|
5
|
+
module Datastores
|
6
|
+
|
7
|
+
# Datastore storing files on Amazon S3.
|
8
|
+
#
|
9
|
+
# It use the file SHA1 digest as a filename so two identical files are not
|
10
|
+
# stored twice.
|
11
|
+
# Each files can be accessed afterwards using a public HTTP(S) address.
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
# datastore = Staticd::Datastores::S3.new(
|
15
|
+
# host: bucket_name,
|
16
|
+
# username: access_key_id,
|
17
|
+
# password: secret_access_key
|
18
|
+
# )
|
19
|
+
# datastore.put(file_path) unless datastore.exist?(file_path)
|
20
|
+
# # => http://bucket_name.hostname/sha1_digest
|
21
|
+
class S3
|
22
|
+
|
23
|
+
def initialize(params)
|
24
|
+
@bucket_name = params[:host]
|
25
|
+
@access_key = ENV["AWS_ACCESS_KEY_ID"]
|
26
|
+
@secret_key = ENV["AWS_SECRET_ACCESS_KEY"]
|
27
|
+
end
|
28
|
+
|
29
|
+
def put(file_path)
|
30
|
+
s3_object = object(file_path)
|
31
|
+
s3_object.write(file: file_path)
|
32
|
+
s3_object.acl = :public_read
|
33
|
+
s3_object.public_url(secure: false).to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def exist?(file_path)
|
37
|
+
s3_object = object(file_path)
|
38
|
+
s3_object.exists? ? s3_object.public_url(secure: false) : false
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def s3
|
44
|
+
@s3 ||= AWS::S3.new(
|
45
|
+
access_key_id: @access_key,
|
46
|
+
secret_access_key: @secret_key
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def bucket
|
51
|
+
@bucket ||= s3.buckets[@bucket_name]
|
52
|
+
end
|
53
|
+
|
54
|
+
def object(file_path)
|
55
|
+
bucket.objects[sha1(file_path)]
|
56
|
+
end
|
57
|
+
|
58
|
+
def sha1(file_path)
|
59
|
+
Digest::SHA1.hexdigest(File.read(file_path))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Staticd
|
2
|
+
|
3
|
+
# Domain name generator.
|
4
|
+
#
|
5
|
+
# This class can be used to generate random words of various length.
|
6
|
+
# Options:
|
7
|
+
# * length: the length of the random word
|
8
|
+
# * suffix: a suffix to append to the random world (default is none)
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
# DomainGenerator.new(length: 2, suffix: ".domain.tld")
|
12
|
+
# # => rb.domain.tld
|
13
|
+
#
|
14
|
+
# A block can be used to validate the generated domain. It must return true
|
15
|
+
# to validate the domain, otherwise a new one is proposed. This feature can
|
16
|
+
# be used to validate the domain against certain rules.
|
17
|
+
#
|
18
|
+
# Example:
|
19
|
+
# DomainGenerator.new(suffix: ".domain.tld") do |generated_domain|
|
20
|
+
# ["admin", "www"].include?(generated_domain)
|
21
|
+
# end
|
22
|
+
class DomainGenerator
|
23
|
+
|
24
|
+
def self.new(options={})
|
25
|
+
if block_given?
|
26
|
+
until domain = generate(options)
|
27
|
+
yield domain
|
28
|
+
end
|
29
|
+
domain
|
30
|
+
else
|
31
|
+
generate(options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.generate(options={})
|
36
|
+
length = options[:length] || 6
|
37
|
+
suffix = options[:suffix] || ""
|
38
|
+
random = ("a".."z").to_a.shuffle[0, length].join
|
39
|
+
random + suffix
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "staticd/database"
|
2
|
+
require "staticd/cache_engine"
|
3
|
+
|
4
|
+
module Staticd
|
5
|
+
|
6
|
+
# Rack middleware to manage remote HTTP resources caching.
|
7
|
+
#
|
8
|
+
# For each request, it will find the last release of the site associed with
|
9
|
+
# the requested domain name and cache the corresponding resource if not
|
10
|
+
# already cached.
|
11
|
+
class HTTPCache
|
12
|
+
include Staticd::Models
|
13
|
+
|
14
|
+
def initialize(http_root, app)
|
15
|
+
@app = app
|
16
|
+
@http_root = http_root
|
17
|
+
|
18
|
+
raise "No HTTP root folder provided" unless @http_root
|
19
|
+
raise "No rack app provided" unless @app
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(env)
|
23
|
+
dup._call(env)
|
24
|
+
end
|
25
|
+
|
26
|
+
def _call(env)
|
27
|
+
@env = env
|
28
|
+
|
29
|
+
# Change the Request Path to '/index.html' if root path is asked.
|
30
|
+
@env["PATH_INFO"] = "/index.html" if @env["PATH_INFO"] == '/'
|
31
|
+
|
32
|
+
# Get the release from the request Host header.
|
33
|
+
release = Release.last(
|
34
|
+
Release.site.domain_names.name => Rack::Request.new(@env).host
|
35
|
+
)
|
36
|
+
return next_middleware unless release
|
37
|
+
|
38
|
+
# Change the script name to include the site name and release version.
|
39
|
+
@env["SCRIPT_NAME"] = "/#{release.site_name}/#{release}"
|
40
|
+
|
41
|
+
req = Rack::Request.new(@env)
|
42
|
+
cache_engine = CacheEngine.new(@http_root)
|
43
|
+
|
44
|
+
# Do nothing else if the resource is already cached.
|
45
|
+
return next_middleware if cache_engine.cached?(req.path)
|
46
|
+
|
47
|
+
# Get the resource to cache.
|
48
|
+
resource = Resource.first(
|
49
|
+
Resource.routes.release_id => release.id,
|
50
|
+
Resource.routes.path => req.path_info
|
51
|
+
)
|
52
|
+
return next_middleware unless resource
|
53
|
+
|
54
|
+
# Cache the resource.
|
55
|
+
cache_engine.cache(req.path, resource.url)
|
56
|
+
next_middleware
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def next_middleware
|
62
|
+
@app.call(@env)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require "sendfile"
|
2
|
+
|
3
|
+
module Staticd
|
4
|
+
|
5
|
+
# Simple HTTP server Rack app.
|
6
|
+
#
|
7
|
+
# If the resource is readable from the root folder at the path given by the
|
8
|
+
# request, the resource is sent to the client. Otherwise a 404 Not Found HTTP
|
9
|
+
# error is sent.
|
10
|
+
class HTTPServer
|
11
|
+
|
12
|
+
# Mime types served by the webserver (from NGiNX mime.types file).
|
13
|
+
EXT_MIME_TYPE = {
|
14
|
+
".html" => "text/html",
|
15
|
+
".htm" => "text/html",
|
16
|
+
".shtml" => "text/html",
|
17
|
+
".css" => "text/css",
|
18
|
+
".xml" => "text/xml",
|
19
|
+
".rss" => "text/xml",
|
20
|
+
".gif" => "image/gif",
|
21
|
+
".jpeg" => "image/jpeg",
|
22
|
+
".jpg" => "image/jpeg",
|
23
|
+
".js" => "application/x-javascript",
|
24
|
+
".txt" => "text/plain",
|
25
|
+
".htc" => "text/x-component",
|
26
|
+
".mml" => "text/mathml",
|
27
|
+
".png" => "image/png",
|
28
|
+
".ico" => "image/x-icon",
|
29
|
+
".jng" => "image/x-jng",
|
30
|
+
".wbmp" => "image/vnd.wap.wbmp",
|
31
|
+
".jar" => "application/java-archive",
|
32
|
+
".war" => "application/java-archive",
|
33
|
+
".ear" => "application/java-archive",
|
34
|
+
".hqx" => "application/mac-binhex40",
|
35
|
+
".pdf" => "application/pdf",
|
36
|
+
".cco" => "application/x-cocoa",
|
37
|
+
".jardiff" => "application/x-java-archive-diff",
|
38
|
+
".jnlp" => "application/x-java-jnlp-file",
|
39
|
+
".run" => "application/x-makeself",
|
40
|
+
".pm" => "application/x-perl",
|
41
|
+
".pl" => "application/x-perl",
|
42
|
+
".prc" => "application/x-pilot",
|
43
|
+
".pdb" => "application/x-pilot",
|
44
|
+
".rar" => "application/x-rar-compressed",
|
45
|
+
".rpm" => "application/x-redhat-package-manager",
|
46
|
+
".sea" => "application/x-sea",
|
47
|
+
".swf" => "application/x-shockwave-flash",
|
48
|
+
".sit" => "application/x-stuffit",
|
49
|
+
".tcl" => "application/x-tcl",
|
50
|
+
".tk" => "application/x-tcl",
|
51
|
+
".der" => "application/x-x509-ca-cert",
|
52
|
+
".pem" => "application/x-x509-ca-cert",
|
53
|
+
".crt" => "application/x-x509-ca-cert",
|
54
|
+
".xpi" => "application/x-xpinstall",
|
55
|
+
".zip" => "application/zip",
|
56
|
+
".deb" => "application/octet-stream",
|
57
|
+
".bin" => "application/octet-stream",
|
58
|
+
".exe" => "application/octet-stream",
|
59
|
+
".dll" => "application/octet-stream",
|
60
|
+
".dmg" => "application/octet-stream",
|
61
|
+
".eot" => "application/octet-stream",
|
62
|
+
".iso" => "application/octet-stream",
|
63
|
+
".img" => "application/octet-stream",
|
64
|
+
".msi" => "application/octet-stream",
|
65
|
+
".msp" => "application/octet-stream",
|
66
|
+
".msm" => "application/octet-stream",
|
67
|
+
".mp3" => "audio/mpeg",
|
68
|
+
".ra" => "audio/x-realaudio",
|
69
|
+
".mpeg" => "video/mpeg",
|
70
|
+
".mpg" => "video/mpeg",
|
71
|
+
".mov" => "video/quicktime",
|
72
|
+
".flv" => "video/x-flv",
|
73
|
+
".avi" => "video/x-msvideo",
|
74
|
+
".wmv" => "video/x-ms-wmv",
|
75
|
+
".asx" => "video/x-ms-asf",
|
76
|
+
".asf" => "video/x-ms-asf",
|
77
|
+
".mng" => "video/x-mng"
|
78
|
+
}
|
79
|
+
|
80
|
+
# Mime type used when no type has been identified.
|
81
|
+
DEFAULT_MIME_TYPE = "application/octet-stream"
|
82
|
+
|
83
|
+
def initialize(http_root, access_logger=nil)
|
84
|
+
@http_root = http_root
|
85
|
+
unless (@access_logger = access_logger)
|
86
|
+
@access_logger = Logger.new(STDOUT)
|
87
|
+
@access_logger.formatter = proc { |_, _, _, msg| "#{msg}\n"}
|
88
|
+
end
|
89
|
+
|
90
|
+
raise "No HTTP root folder provided" unless @http_root
|
91
|
+
end
|
92
|
+
|
93
|
+
def call(env)
|
94
|
+
dup._call(env)
|
95
|
+
end
|
96
|
+
|
97
|
+
def _call(env)
|
98
|
+
@env = env
|
99
|
+
req = Rack::Request.new(@env)
|
100
|
+
file_path = @http_root + req.path
|
101
|
+
res = File.readable?(file_path) ? serve(file_path) : send_404
|
102
|
+
log(req, res)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# Log a request using the "Extended Log File Format".
|
108
|
+
# See: http://www.w3.org/TR/WD-logfile.html
|
109
|
+
#
|
110
|
+
# Use the request.time key setup by the Rack::RequestTime middleware.
|
111
|
+
#
|
112
|
+
# Version: 1.0
|
113
|
+
# Fields: time cs-dns cs-ip date cs-method cs-uri sc-status sc-byte sc-time-taken
|
114
|
+
def log(req, res)
|
115
|
+
request_stop_time = Time.now
|
116
|
+
request_start_time =
|
117
|
+
req.env.key?("request.time") ? req.env["request.time"] : nil
|
118
|
+
request_completed_time =
|
119
|
+
if request_start_time
|
120
|
+
(request_stop_time - request_start_time).round(4)
|
121
|
+
else
|
122
|
+
"-"
|
123
|
+
end
|
124
|
+
content_length =
|
125
|
+
res[1].key?("Content-Length") ? " #{res[1]["Content-Length"]}" : "-"
|
126
|
+
|
127
|
+
log_string = "#{request_stop_time.strftime("%Y-%m-%d %H:%M:%S")}"
|
128
|
+
log_string << " #{req.host}"
|
129
|
+
log_string << " #{req.env["REMOTE_ADDR"]}"
|
130
|
+
log_string << " #{req.env["REQUEST_METHOD"]} #{req.path_info}"
|
131
|
+
log_string << " #{res[0]}"
|
132
|
+
log_string << content_length
|
133
|
+
log_string << " #{request_completed_time}"
|
134
|
+
@access_logger.info(log_string)
|
135
|
+
|
136
|
+
res
|
137
|
+
end
|
138
|
+
|
139
|
+
# Serve a file.
|
140
|
+
#
|
141
|
+
# This method will return a Rack compatible response ready to be served to
|
142
|
+
# the client. It will use the appropriate method (loading file into memory
|
143
|
+
# vs serving file using the sendfile system call) based on availability.
|
144
|
+
def serve(file_path)
|
145
|
+
@env['rack.hijack?'] ? sendfile(file_path) : send(file_path)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Send a file loading it in memory.
|
149
|
+
#
|
150
|
+
# Method used in the first implementation.
|
151
|
+
# Keep it for compatibility purpose when the Rack hijack API is not
|
152
|
+
# supported.
|
153
|
+
def send(file_path)
|
154
|
+
response = Rack::Response.new
|
155
|
+
response["Content-Type"] = mime(file_path)
|
156
|
+
File.foreach(file_path) { |chunk| response.write(chunk) }
|
157
|
+
response.finish
|
158
|
+
end
|
159
|
+
|
160
|
+
# Use sendfile system call to send file without loading it into memory.
|
161
|
+
#
|
162
|
+
# It use the sendfile gem and the rack hijacking api.
|
163
|
+
# See: https://github.com/codeslinger/sendfile
|
164
|
+
# See: http://blog.phusion.nl/2013/01/23/the-new-rack-socket-hijacking-api/
|
165
|
+
def sendfile(file_path)
|
166
|
+
|
167
|
+
response_header = {
|
168
|
+
"Content-Type" => mime(file_path),
|
169
|
+
"Content-Length" => size(file_path),
|
170
|
+
"Connection" => "close"
|
171
|
+
}
|
172
|
+
response_header["rack.hijack"] = lambda do |io|
|
173
|
+
begin
|
174
|
+
File.open(file_path) { |file| io.sendfile(file) }
|
175
|
+
io.flush
|
176
|
+
ensure
|
177
|
+
io.close
|
178
|
+
end
|
179
|
+
end
|
180
|
+
[200, response_header]
|
181
|
+
end
|
182
|
+
|
183
|
+
def send_404
|
184
|
+
res = Rack::Response.new(["Not Found"], 404, {})
|
185
|
+
res.finish
|
186
|
+
end
|
187
|
+
|
188
|
+
def mime(file_path)
|
189
|
+
ext = File.extname(file_path).downcase
|
190
|
+
EXT_MIME_TYPE.key?(ext) ? EXT_MIME_TYPE[ext] : DEFAULT_MIME_TYPE
|
191
|
+
end
|
192
|
+
|
193
|
+
def size(file_path)
|
194
|
+
File.size(file_path).to_s
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Staticd
|
4
|
+
|
5
|
+
# Simple HTTP response constructor for JSON content.
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
# response = JSONResponse.send(:success, {foo: :bar})
|
9
|
+
class JSONResponse
|
10
|
+
|
11
|
+
def self.send(type, content=nil)
|
12
|
+
case type
|
13
|
+
when :success
|
14
|
+
@status = 200
|
15
|
+
@body = content
|
16
|
+
when :success_no_content
|
17
|
+
@status = 204
|
18
|
+
when :error
|
19
|
+
@status = 403
|
20
|
+
@body = {error: content}
|
21
|
+
else
|
22
|
+
@status = 500
|
23
|
+
@body = {error: "Something went wrong on our side."}
|
24
|
+
end
|
25
|
+
json_body = @body ? JSON.generate(@body) : nil
|
26
|
+
[@status, json_body]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|