Class: Pinion::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/pinion/server.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(mount_point) ⇒ Server

Returns a new instance of Server.



13
14
15
# File 'lib/pinion/server.rb', line 13

def initialize(mount_point)
  @mount_point = mount_point
end

Instance Attribute Details

#mount_pointObject (readonly)

Returns the value of attribute mount_point.



11
12
13
# File 'lib/pinion/server.rb', line 11

def mount_point
  @mount_point
end

Instance Method Details

#asset_inline(path) ⇒ Object



128
# File 'lib/pinion/server.rb', line 128

def asset_inline(path) Asset[path].contents end

#asset_url(path) ⇒ Object

Helper methods for an application to generate urls (with fingerprints in production)



112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/pinion/server.rb', line 112

def asset_url(path)
  path.sub!(%r[^(#{@mount_point})?/?], "")
  mounted_path = "#{@mount_point}/#{path}"

  return mounted_path unless Pinion.environment == "production"

  # Add on a checksum tag in production
  asset = Asset[path]
  raise "Error: no such asset available: #{path}" unless asset
  mounted_path, dot, extension = mounted_path.rpartition(".")
  return mounted_path if dot.empty?
  "#{mounted_path}-#{asset.checksum}.#{extension}"
end

#bundle_url(name) ⇒ Object

Return the bundle url. In production, the single bundled result is produced; otherwise, each individual asset_url is returned.



148
149
150
151
152
153
# File 'lib/pinion/server.rb', line 148

def bundle_url(name)
  bundle = Bundle[name]
  raise "No such bundle: #{name}" unless bundle
  return bundle.paths.map { |p| asset_url(p) } unless Pinion.environment == "production"
  ["#{@mount_point}/#{bundle.name}-#{bundle.checksum}.#{bundle.extension}"]
end

#call(env) ⇒ Object



45
46
47
48
49
50
51
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
104
105
106
107
108
109
# File 'lib/pinion/server.rb', line 45

def call(env)
  # Avoid modifying the session state, don't set cookies, etc
  env["rack.session.options"] ||= {}
  env["rack.session.options"].merge! :defer => true, :skip => true

  root = env["SCRIPT_NAME"]
  path = Rack::Utils.unescape(env["PATH_INFO"].to_s).sub(%r[^/], "")

  if path.include? ".."
    return [403, { "Content-Type" => "text/plain", "Content-Length" => "9" }, ["Forbidden"]]
  end

  # Pull out the md5sum if it's part of the given path
  # e.g. foo/bar-a95c53a7a0f5f492a74499e70578d150.js -> a95c53a7a0f5f492a74499e70578d150
  checksum_tag = bundle_name = nil
  matches = path.match /([^\/]+)-([\da-f]{32})\..+$/
  if matches && matches.size == 3
    bundle_name = matches[1]
    checksum_tag = matches[2]
    path.sub!("-#{checksum_tag}", "")
  end

  # First see if there is a bundle with this name
  asset = Bundle[bundle_name]
  if asset
    # If the checksum doesn't match the bundle, we want to return a 404.
    asset = nil unless asset.checksum == checksum_tag
  else
    # If there is no bundle with this name, find another type of Asset with the name instead.
    asset = Asset[path]
  end

  if asset
    # If the ETag matches, give a 304
    return [304, {}, []] if env["HTTP_IF_NONE_MATCH"] == %Q["#{asset.checksum}"]

    if Pinion.environment == "production"
      # In production, if there is a checksum, cache for a year (because if the asset changes with a
      # redeploy, the checksum will change). If there is no checksum, cache for 10 minutes (this way at
      # worst we only serve 10 minute stale assets, and caches in front of Pinion will be able to help
      # out).
      cache_policy = checksum_tag ? "max-age=31536000" : "max-age=600"
    else
      # Don't cache in dev.
      cache_policy = "must-revalidate"
    end
    headers = {
      "Content-Type" => asset.content_type,
      "Content-Length" => asset.length.to_s,
      "ETag" => %Q["#{asset.checksum}"],
      "Cache-Control" => "public, #{cache_policy}",
      "Last-Modified" => asset.mtime.httpdate,
    }
    body = env["REQUEST_METHOD"] == "HEAD" ? [] : asset
    [200, headers, body]
  else
    [404, { "Content-Type" => "text/plain", "Content-Length" => "9" }, ["Not found"]]
  end
rescue Exception => e
  # TODO: logging
  STDERR.puts "Error compiling #{path}:"
  STDERR.puts "#{e.class.name}: #{e.message}"
  # TODO: render nice custom errors in the browser
  raise
end

#convert(from_and_to, &block) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/pinion/server.rb', line 17

def convert(from_and_to, &block)
  unless from_and_to.is_a?(Hash) && from_and_to.size == 1
    raise Error, "Unexpected argument to convert: #{from_and_to.inspect}"
  end
  from, to = from_and_to.to_a[0]
  unless [from, to].all? { |s| s.is_a? Symbol }
    raise Error, "Expecting symbols in this hash #{from_and_to.inspect}"
  end
  if block_given?
    # Save new conversion type (this might overwrite an implicit or previously defined conversion)
    Conversion.create(from_and_to) do
      render { |file_contents| block.call(file_contents) }
    end
  else
    unless Conversion[from_and_to]
      raise Error, "No implicit conversion for #{from_and_to.inspect}. Must provide a conversion block."
    end
  end
end

#create_bundle(name, bundle_name, paths) ⇒ Object

Create a bundle of assets. Each asset must convert to the same final type. ‘name` is an identifier for this bundle; `bundle_name` is the # identifier for your bundle type (e.g. `:concatenate_and_uglify_js`); and `paths` are all the asset paths. In development, no bundles will be created (but the list of discrete assets will be saved for use in a subsequent `bundle_url` call).



136
137
138
139
140
141
142
143
144
# File 'lib/pinion/server.rb', line 136

def create_bundle(name, bundle_name, paths)
  if Bundle[name]
    raise "Error: there is already a bundle called #{name} with different component files. Each " <<
          "bundle must have a unique name."
  end

  normalized_paths = paths.map { |path| path.sub(%r[^(#{@mount_point})?/?], "") }
  Bundle.create(name, bundle_name, normalized_paths)
end

#css_bundle(name) ⇒ Object



155
# File 'lib/pinion/server.rb', line 155

def css_bundle(name) bundle_url(name).map { |path| css_wrapper(path) }.join end

#css_inline(path) ⇒ Object



129
# File 'lib/pinion/server.rb', line 129

def css_inline(path) %Q{<style type="text/css">#{asset_inline(path)}</style>} end

#css_url(path) ⇒ Object



125
# File 'lib/pinion/server.rb', line 125

def css_url(path) css_wrapper(asset_url(path)) end

#js_bundle(name) ⇒ Object



154
# File 'lib/pinion/server.rb', line 154

def js_bundle(name) bundle_url(name).map { |path| js_wrapper(path) }.join end

#js_inline(path) ⇒ Object



130
# File 'lib/pinion/server.rb', line 130

def js_inline(path) %Q{<script>#{asset_inline(path)}</script>} end

#js_url(path) ⇒ Object



126
# File 'lib/pinion/server.rb', line 126

def js_url(path) js_wrapper(asset_url(path)) end

#watch(path) ⇒ Object

Raises:



37
38
39
40
41
# File 'lib/pinion/server.rb', line 37

def watch(path)
  raise Error, "#{path} is not a directory." unless File.directory? path
  Asset.watch_path(path)
  Conversion.add_watch_directory path
end