Class: Capsium::Reactor::Mount

Inherits:
Object
  • Object
show all
Defined in:
lib/capsium/reactor/mount.rb,
sig/capsium/reactor/mount.rbs

Overview

One mounted package: the URL prefix it answers under plus its serving state. The reactor serves a list of mounts resolved by longest-prefix matching; default mount points are "/" for the first source and "/<metadata.name>/" for each additional one.

Defined Under Namespace

Classes: Entry

Constant Summary collapse

ROOT_PATH =

Returns:

  • (String)
"/"
SPEC_SEPARATOR =

Returns:

  • (String)
"="

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path:, package:, store: nil, registry: nil, workdir: nil) ⇒ Mount

Returns a new instance of Mount.

Parameters:



114
115
116
117
118
119
120
# File 'lib/capsium/reactor/mount.rb', line 114

def initialize(path:, package:, store: nil, registry: nil, workdir: nil)
  @path = self.class.normalize_path(path)
  @store = store
  @registry = registry
  @package = self.class.load_package(package, store, registry)
  attach_workdir(workdir) if workdir
end

Instance Attribute Details

#overlayOverlay? (readonly)

Returns the value of attribute overlay.

Returns:



26
27
28
# File 'lib/capsium/reactor/mount.rb', line 26

def overlay
  @overlay
end

#packagePackage (readonly)

Returns the value of attribute package.

Returns:



26
27
28
# File 'lib/capsium/reactor/mount.rb', line 26

def package
  @package
end

#pathString (readonly)

Returns the value of attribute path.

Returns:

  • (String)


26
27
28
# File 'lib/capsium/reactor/mount.rb', line 26

def path
  @path
end

Class Method Details

.build(entries, store: nil, registry: nil) ⇒ Array[Mount]

Builds the mounts for a list of entries: loads each package, assigns default paths (first entry "/", each additional "/<metadata.name>/") and rejects duplicate prefixes with a MountConflictError. On failure every package loaded so far is cleaned up again.

Parameters:

Returns:



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/capsium/reactor/mount.rb', line 66

def self.build(entries, store: nil, registry: nil)
  built = []
  entries.each_with_index do |entry, index|
    package_store = entry.store || store
    package = load_package(entry.source, package_store, registry)
    path = entry.path || default_path(index, package)
    built << new(path: path, package: package,
                 store: package_store, registry: registry)
  end
  check_conflicts!(built)
  built
rescue StandardError
  built.each { |mount| mount.package.cleanup }
  raise
end

.config_entries(config_file) ⇒ Array[Entry]

The mount entries of a JSON config file: [{"path": "/", "source": "dir-or.cap", "store": "..."]}

Parameters:

  • config_file (String)

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/capsium/reactor/mount.rb', line 40

def self.config_entries(config_file)
  doc = JSON.parse(File.read(config_file))
  mounts = doc.is_a?(Hash) ? doc["mounts"] : nil
  unless mounts.is_a?(Array) && mounts.all?(Hash)
    raise Error, "Invalid mount config #{config_file}: expected " \
                 '{"mounts": [{"path": ..., "source": ...}]}'
  end

  mounts.map do |entry|
    unless entry["source"].is_a?(String)
      raise Error, "Invalid mount config #{config_file}: every mount " \
                   "needs a \"source\""
    end

    Entry.new(path: entry["path"], source: entry["source"],
              store: entry["store"])
  end
rescue JSON::ParserError => e
  raise Error, "Invalid mount config #{config_file}: #{e.message}"
end

.load_package(source, store, registry) ⇒ Package

Loads a mount source (a package directory, .cap file or an already-built Package) with the given resolution context.

Parameters:

Returns:



84
85
86
87
88
# File 'lib/capsium/reactor/mount.rb', line 84

def self.load_package(source, store, registry)
  return source unless source.is_a?(String)

  Package.new(source, store: store, registry: registry)
end

.normalize_path(path) ⇒ String

Canonical mount prefix: leading "/", no trailing "/" (except the root itself).

Parameters:

  • path (String, nil)

Returns:

  • (String)


107
108
109
110
111
112
# File 'lib/capsium/reactor/mount.rb', line 107

def self.normalize_path(path)
  return ROOT_PATH if path.nil? || path == ROOT_PATH

  normalized = path.start_with?(ROOT_PATH) ? path : "#{ROOT_PATH}#{path}"
  normalized.chomp(ROOT_PATH)
end

.parse_spec(spec) ⇒ Entry

Parses a "--mount PATH=SOURCE" command-line value into an Entry.

Parameters:

  • spec (String)

Returns:



29
30
31
32
33
34
35
36
# File 'lib/capsium/reactor/mount.rb', line 29

def self.parse_spec(spec)
  path, separator, source = spec.to_s.partition(SPEC_SEPARATOR)
  if separator.empty? || path.empty? || source.empty?
    raise Error, "Invalid mount spec #{spec.inspect} (expected PATH=SOURCE)"
  end

  Entry.new(path: path, source: source, store: nil)
end

Instance Method Details

#attach_workdir(workdir) ⇒ void

This method returns an undefined value.

Attaches the reactor workdir (creates the writable overlay).

Parameters:

  • workdir (String)


125
126
127
# File 'lib/capsium/reactor/mount.rb', line 125

def attach_workdir(workdir)
  @overlay = Overlay.new(root: File.join(workdir, "overlays", @package.name))
end

#content_apiContentApi

Returns:



173
# File 'lib/capsium/reactor/mount.rb', line 173

def content_api = @content_api ||= ContentApi.new(self)

#data_apiDataApi

Returns:



171
# File 'lib/capsium/reactor/mount.rb', line 171

def data_api = @data_api ||= DataApi.new(self)

#dataset_data(dataset) ⇒ Object

The dataset as served: base data merged with overlay mutations.

Parameters:

Returns:

  • (Object)


167
168
169
# File 'lib/capsium/reactor/mount.rb', line 167

def dataset_data(dataset)
  @overlay ? @overlay.dataset_data(dataset) : dataset.data
end

#graphql?Boolean

Whether this mount exposes a GraphQL API (any file-backed dataset).

Returns:

  • (Boolean)


177
178
179
# File 'lib/capsium/reactor/mount.rb', line 177

def graphql?
  @package.storage.datasets.any? { |dataset| !dataset.config.sqlite? }
end

#graphql_apiGraphqlApi

Returns:



181
# File 'lib/capsium/reactor/mount.rb', line 181

def graphql_api = @graphql_api ||= GraphqlApi.new(self)

#inner_path(request_path) ⇒ String

The package-local path for a request path under this mount ("/" when the request hits the mount prefix itself).

Parameters:

  • request_path (String)

Returns:

  • (String)


144
145
146
147
148
149
150
# File 'lib/capsium/reactor/mount.rb', line 144

def inner_path(request_path)
  inner = request_path.delete_prefix(path)
  return ROOT_PATH if inner.empty?
  return inner if inner.start_with?(ROOT_PATH)

  "#{ROOT_PATH}#{inner}"
end

#matches?(request_path) ⇒ Boolean

Whether this mount answers the given request path: the prefix itself or anything below it. The root mount answers everything.

Parameters:

  • request_path (String)

Returns:

  • (Boolean)


137
138
139
140
# File 'lib/capsium/reactor/mount.rb', line 137

def matches?(request_path)
  path == ROOT_PATH || request_path == path ||
    request_path.start_with?("#{path}#{ROOT_PATH}")
end

#merged_viewPackage::MergedView

The merged content view; the overlay is always the topmost layer when a workdir is attached (hot-swap: content writes and tombstones resolve on the next request).

Returns:



157
158
159
160
161
162
163
# File 'lib/capsium/reactor/mount.rb', line 157

def merged_view
  @merged_view ||= if @overlay
                     @package.merged_view(extra_layers: [@overlay.layer])
                   else
                     @package.merged_view
                   end
end

#reloadPackage

Reloads the package from its (prepared) path, e.g. after the filesystem listener noticed changes. The overlay survives (it lives in the reactor workdir, not in the package).

Returns:



199
200
201
202
203
# File 'lib/capsium/reactor/mount.rb', line 199

def reload
  @merged_view = nil
  @graphql_api = nil
  @package = Package.new(@package.path, store: @store, registry: @registry)
end

#routesPackage::Routes

Returns:



152
# File 'lib/capsium/reactor/mount.rb', line 152

def routes = @package.routes

#server_pathsArray[String]

The WEBrick mount points serving this mount.

Returns:

  • (Array[String])


190
191
192
193
194
# File 'lib/capsium/reactor/mount.rb', line 190

def server_paths
  return [path] unless path == ROOT_PATH

  routes.config.routes.map(&:serving_path)
end

#summaryString

The one-line "name version" summary used in reactor logs.

Returns:

  • (String)


184
# File 'lib/capsium/reactor/mount.rb', line 184

def summary = "#{@package.name} #{@package.metadata.version}"

#writable?Boolean

Writable unless the package declares "readOnly": true.

Returns:

  • (Boolean)


131
132
133
# File 'lib/capsium/reactor/mount.rb', line 131

def writable?
  !@overlay.nil? && @package..read_only != true
end