Class: Scarpe::Components::AssetServer

Inherits:
Object
  • Object
show all
Includes:
Base64, Shoes::Log
Defined in:
scarpe-components/lib/scarpe/components/asset_server.rb

Defined Under Namespace

Classes: FileServlet

Constant Summary collapse

URL_TYPES =
[:auto, :asset, :data]

Constants included from Shoes::Log

Shoes::Log::DEFAULT_COMPONENT, Shoes::Log::DEFAULT_DEBUG_LOG_CONFIG, Shoes::Log::DEFAULT_LOG_CONFIG

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Shoes::Log

configure_logger, #log_init, logger

Methods included from Base64

#encode_file_to_base64, #mime_type_for_filename, #valid_url?

Constructor Details

#initialize(port: 0, app_dir:, never_start_server: false, connect_timeout: 5) ⇒ AssetServer

Port 0 will auto-assign a free port



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'scarpe-components/lib/scarpe/components/asset_server.rb', line 17

def initialize(port: 0, app_dir:, never_start_server: false, connect_timeout: 5)
  log_init("AssetServer")

  require "scarpe/components/base64"

  @server_started = false
  @server_thread = nil
  @port = port != 0 ? port : find_open_port
  @app_dir = File.expand_path app_dir
  @components_dir = File.expand_path "#{__dir__}/../../.."
  @connect_timeout = connect_timeout
  @never_start_server = never_start_server

  # For now, always use 16kb as the cutoff for preferring to serve a file with the asset server
  @auto_asset_url_size = 16 * 1024

  # Make sure the child process is dead
  at_exit do
    kill_server
  end
end

Instance Attribute Details

#dirObject (readonly)

Returns the value of attribute dir.



12
13
14
# File 'scarpe-components/lib/scarpe/components/asset_server.rb', line 12

def dir
  @dir
end

#portObject (readonly)

Returns the value of attribute port.



10
11
12
# File 'scarpe-components/lib/scarpe/components/asset_server.rb', line 10

def port
  @port
end

#server_startedObject (readonly)

Returns the value of attribute server_started.



11
12
13
# File 'scarpe-components/lib/scarpe/components/asset_server.rb', line 11

def server_started
  @server_started
end

Instance Method Details

#asset_url(url, url_type: :auto) ⇒ Object

Get an asset URL for the given url or filename. The asset server can return a data URL, which encodes the entire file into the URL. It can return an asset server URL, which will serve the file via a local webrick server (@see AssetServer).

If url_type is auto, asset_url will return a data URL or asset server URL depending on file size and whether it's local to the asset server. Remote URLs will always be returned verbatim.

Parameters:

  • url (String)

    the filename or URL

  • url_type (Symbol) (defaults to: :auto)

    the type of URL to return - one of :auto, :asset, :data



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'scarpe-components/lib/scarpe/components/asset_server.rb', line 52

def asset_url(url, url_type: :auto)
  unless URL_TYPES.include?(url_type)
    raise ArgumentError, "The url_type arg must be one of #{URL_TYPES.inspect}!"
  end

  if valid_url?(url)
    # This is already not local, use it directly
    return url
  end

  # Invalid URLs are assumed to be file paths.
  url = File.expand_path url
  file_size = File.size(url)

  # Calculate the app-relative path to the file. If it's not outside the app
  # dir, great, use that. If it *is* outside the app dir, see if the
  # scarpe-components dir is better (e.g. for Tiranti Bootstrap CSS assets.)
  relative_app_path = relative_path_from_to(@app_dir, url)
  relative_path = relative_app_path
  if relative_app_path.start_with?("../")
    relative_comp_path = relative_path_from_to(@components_dir, url)
    relative_path = relative_comp_path unless relative_comp_path.start_with?("../")
  end

  # If url_type is :auto, we will use a data URL for small files and files that
  # would be outside the asset server's directory. Data URLs are less efficient
  # for large files, but we'll try to always serve *something* if we can.
  if url_type == :data ||
    (url_type == :auto && file_size < @auto_asset_url_size) ||
    (url_type == :auto && relative_path.start_with?("../"))

    # The MIME media type for this file
    file_type = mime_type_for_filename(url)

    # Up to 16kb per file, inline it directly to avoid an extra HTTP request
    return "data:#{file_type};base64,#{encode_file_to_base64(url)}"
  end

  # Start the server if we're returning an asset-server URL
  unless @server_started || @never_start_server
    start_server_thread
  end

  if relative_path.start_with?("../")
    raise Scarpe::OperationNotAllowedError, "Large asset is outside of application directory and asset URL was requested: #{url.inspect}"
  end
  if relative_path == relative_app_path
    "http://127.0.0.1:#{@port}/app/#{relative_path}"
  else
    "http://127.0.0.1:#{@port}/comp/#{relative_path}"
  end
end

#find_open_portObject



105
106
107
108
109
110
111
# File 'scarpe-components/lib/scarpe/components/asset_server.rb', line 105

def find_open_port
  require "socket"
  s = TCPServer.new('127.0.0.1', port)
  port = s.addr[1]
  s.close
  port
end

#kill_serverObject



167
168
169
170
171
172
173
174
# File 'scarpe-components/lib/scarpe/components/asset_server.rb', line 167

def kill_server
  return unless @server_started && @server_thread

  @server.shutdown
  @server_thread.join if @server_thread.alive?
  @server_started = false
  @server_thread = nil
end

#port_is_responding?(port, timeout: 0.1) ⇒ Boolean

Returns:

  • (Boolean)


113
114
115
# File 'scarpe-components/lib/scarpe/components/asset_server.rb', line 113

def port_is_responding?(port, timeout: 0.1)
  Socket.tcp("127.0.0.1", port, connect_timeout: timeout) { true } rescue false
end

#relative_path_from_to(from, to) ⇒ Object



128
129
130
131
# File 'scarpe-components/lib/scarpe/components/asset_server.rb', line 128

def relative_path_from_to(from, to)
  require 'pathname'
  Pathname.new(to).relative_path_from(Pathname.new from).to_s
end

#retry_port(port, timeout: 2.0) ⇒ Object



117
118
119
120
121
122
123
124
125
126
# File 'scarpe-components/lib/scarpe/components/asset_server.rb', line 117

def retry_port(port, timeout: 2.0)
  t = Time.now
  loop do
    resp = port_is_responding?(port)

    return true if resp
    return false if Time.now - t > timeout
    sleep 0.1
  end
end

#start_server_threadObject



133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'scarpe-components/lib/scarpe/components/asset_server.rb', line 133

def start_server_thread
  return if @server_started

  @server_thread = Thread.new do
    start_webrick
  end
  @server_started = true

  # Give the asset server a couple of seconds to respond
  retry_port(@port, timeout: @connect_timeout)
  unless port_is_responding?(@port, timeout: 0.1)
    @log.warn "Asset server port doesn't seem to be responding after #{@connect_timeout} seconds!"
  end
end

#start_webrickObject



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'scarpe-components/lib/scarpe/components/asset_server.rb', line 148

def start_webrick
  require "tempfile"
  log = WEBrick::Log.new Tempfile.new("scarpe_asset_server_log")
  access_log = [
    [Tempfile.new("scarpe_asset_server_access_log"), WEBrick::AccessLog::COMBINED_LOG_FORMAT]
  ]

  @server = WEBrick::HTTPServer.new(Port: @port, DocumentRoot: @app_dir, Logger: log, AccessLog: access_log)
  @server.mount('/app', FileServlet, { Type: :app, Prefix: "/app", DocumentRoot: @app_dir })
  @server.mount('/comp', FileServlet, { Type: :scarpe_components, Prefix: "/comp", DocumentRoot: @components_dir })

  @server.start

  # Set up a signal trap to gracefully shut down the server on interrupt (e.g., Ctrl+C)
  trap('INT') do
    @server.shutdown
  end
end