capsium 0.1.2 → 0.3.0
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 +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +46 -7
- data/CHANGELOG.md +183 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.adoc +496 -127
- data/Rakefile +8 -1
- data/bin/console +1 -1
- data/capsium.gemspec +12 -19
- data/exe/capsium +1 -1
- data/lib/capsium/cli/convert.rb +18 -0
- data/lib/capsium/cli/formatting.rb +27 -0
- data/lib/capsium/cli/package.rb +148 -0
- data/lib/capsium/cli/reactor.rb +33 -0
- data/lib/capsium/cli.rb +4 -96
- data/lib/capsium/converters/jekyll.rb +5 -7
- data/lib/capsium/converters.rb +7 -0
- data/lib/capsium/package/authentication.rb +38 -0
- data/lib/capsium/package/authentication_config.rb +78 -0
- data/lib/capsium/package/cipher.rb +197 -0
- data/lib/capsium/package/composition.rb +77 -0
- data/lib/capsium/package/dataset.rb +67 -34
- data/lib/capsium/package/dependency_resolver.rb +61 -0
- data/lib/capsium/package/encryption_config.rb +38 -0
- data/lib/capsium/package/manifest.rb +33 -22
- data/lib/capsium/package/manifest_config.rb +52 -7
- data/lib/capsium/package/merged_view.rb +168 -0
- data/lib/capsium/package/metadata.rb +4 -8
- data/lib/capsium/package/metadata_config.rb +97 -9
- data/lib/capsium/package/preparation.rb +81 -0
- data/lib/capsium/package/routes.rb +39 -41
- data/lib/capsium/package/routes_config.rb +174 -44
- data/lib/capsium/package/security.rb +140 -0
- data/lib/capsium/package/security_config.rb +60 -0
- data/lib/capsium/package/signer.rb +201 -0
- data/lib/capsium/package/storage.rb +20 -45
- data/lib/capsium/package/storage_config.rb +128 -3
- data/lib/capsium/package/store.rb +109 -0
- data/lib/capsium/package/testing/config_test.rb +70 -0
- data/lib/capsium/package/testing/context.rb +11 -0
- data/lib/capsium/package/testing/data_validation_test.rb +63 -0
- data/lib/capsium/package/testing/file_test.rb +45 -0
- data/lib/capsium/package/testing/report.rb +33 -0
- data/lib/capsium/package/testing/route_test.rb +64 -0
- data/lib/capsium/package/testing/test_case.rb +60 -0
- data/lib/capsium/package/testing/test_suite.rb +86 -0
- data/lib/capsium/package/testing.rb +20 -0
- data/lib/capsium/package/validator.rb +160 -0
- data/lib/capsium/package/verification.rb +32 -0
- data/lib/capsium/package/version.rb +204 -0
- data/lib/capsium/package.rb +92 -106
- data/lib/capsium/packager.rb +75 -6
- data/lib/capsium/reactor/authenticator.rb +180 -0
- data/lib/capsium/reactor/deploy.rb +71 -0
- data/lib/capsium/reactor/htpasswd.rb +154 -0
- data/lib/capsium/reactor/introspection.rb +91 -0
- data/lib/capsium/reactor/oauth2.rb +106 -0
- data/lib/capsium/reactor/serving.rb +75 -0
- data/lib/capsium/reactor/session.rb +82 -0
- data/lib/capsium/reactor.rb +93 -43
- data/lib/capsium/thor_ext.rb +1 -1
- data/lib/capsium/version.rb +1 -1
- data/lib/capsium.rb +7 -9
- data/sig/capsium/package/authentication.rbs +23 -0
- data/sig/capsium/package/authentication_config.rbs +71 -0
- data/sig/capsium/package/cipher.rbs +51 -0
- data/sig/capsium/package/composition.rbs +20 -0
- data/sig/capsium/package/dataset.rbs +28 -0
- data/sig/capsium/package/dependency_resolver.rbs +49 -0
- data/sig/capsium/package/encryption_config.rbs +35 -0
- data/sig/capsium/package/manifest.rbs +34 -0
- data/sig/capsium/package/manifest_config.rbs +38 -0
- data/sig/capsium/package/merged_view.rbs +46 -0
- data/sig/capsium/package/metadata.rbs +26 -0
- data/sig/capsium/package/metadata_config.rbs +49 -0
- data/sig/capsium/package/preparation.rbs +23 -0
- data/sig/capsium/package/routes.rbs +37 -0
- data/sig/capsium/package/routes_config.rbs +107 -0
- data/sig/capsium/package/security.rbs +72 -0
- data/sig/capsium/package/security_config.rbs +57 -0
- data/sig/capsium/package/signer.rbs +59 -0
- data/sig/capsium/package/storage.rbs +28 -0
- data/sig/capsium/package/storage_config.rbs +85 -0
- data/sig/capsium/package/store.rbs +37 -0
- data/sig/capsium/package/testing/config_test.rbs +21 -0
- data/sig/capsium/package/testing/context.rbs +14 -0
- data/sig/capsium/package/testing/data_validation_test.rbs +22 -0
- data/sig/capsium/package/testing/file_test.rbs +17 -0
- data/sig/capsium/package/testing/report.rbs +20 -0
- data/sig/capsium/package/testing/route_test.rbs +22 -0
- data/sig/capsium/package/testing/test_case.rbs +41 -0
- data/sig/capsium/package/testing/test_suite.rbs +20 -0
- data/sig/capsium/package/testing.rbs +7 -0
- data/sig/capsium/package/validator.rbs +29 -0
- data/sig/capsium/package/verification.rbs +24 -0
- data/sig/capsium/package/version.rbs +39 -0
- data/sig/capsium/package.rbs +61 -0
- data/sig/capsium/packager.rbs +32 -0
- data/sig/capsium/reactor/authenticator.rbs +40 -0
- data/sig/capsium/reactor/deploy.rbs +34 -0
- data/sig/capsium/reactor/htpasswd.rbs +36 -0
- data/sig/capsium/reactor/introspection.rbs +31 -0
- data/sig/capsium/reactor/oauth2.rbs +31 -0
- data/sig/capsium/reactor/serving.rbs +22 -0
- data/sig/capsium/reactor/session.rbs +36 -0
- data/sig/capsium/reactor.rbs +35 -0
- data/sig/capsium.rbs +3 -1
- metadata +108 -170
- data/lib/capsium/package/dataset_config.rb +0 -28
- data/lib/capsium/protector.rb +0 -103
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Capsium
|
|
6
|
+
class Package
|
|
7
|
+
# The merged (overlay) view of a package's content, per ARCHITECTURE.md
|
|
8
|
+
# section 5a. Shared by Package (validation) and Reactor (serving).
|
|
9
|
+
#
|
|
10
|
+
# The content/ tree is always the implicit bottom layer; layers from
|
|
11
|
+
# storage.layers stack on top of it, bottom -> top in declaration
|
|
12
|
+
# order. Resolution scans from the TOP down and the first hit wins.
|
|
13
|
+
# Deletions are recorded as tombstones: a `.capsium-tombstones` JSON
|
|
14
|
+
# file in a writable layer listing content/-relative paths; a
|
|
15
|
+
# tombstoned path resolves to nil even when a lower layer (including a
|
|
16
|
+
# dependency's content) holds the file, while a file reappearing in a
|
|
17
|
+
# layer ABOVE the tombstone is served again.
|
|
18
|
+
#
|
|
19
|
+
# Composite packages (section 4a): `dependency_views` are
|
|
20
|
+
# [dependency GUID, MergedView] pairs acting as read-only layers below
|
|
21
|
+
# ALL own layers. A resource reference that is a URI of the form
|
|
22
|
+
# "<dependency-guid>/<path>" resolves explicitly against that
|
|
23
|
+
# dependency's view; plain content paths fall through to the
|
|
24
|
+
# dependency layers in declaration order after all own layers miss.
|
|
25
|
+
#
|
|
26
|
+
# With `exported_only: true` (the view a dependent package gets):
|
|
27
|
+
# layers whose visibility is "private" are hidden entirely, and a path
|
|
28
|
+
# resolves only when the manifest lists the resource as "exported".
|
|
29
|
+
class MergedView
|
|
30
|
+
# A single read layer: an absolute root directory mirroring the
|
|
31
|
+
# content/ tree, plus its parsed tombstone set.
|
|
32
|
+
Layer = Data.define(:root, :visibility, :tombstones) do
|
|
33
|
+
def file?(content_relative_path)
|
|
34
|
+
File.file?(absolute(content_relative_path))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def absolute(content_relative_path)
|
|
38
|
+
File.join(root, content_relative_path)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
TOMBSTONE_FILE = ".capsium-tombstones"
|
|
43
|
+
CONTENT_PREFIX = "#{Package::CONTENT_DIR}/".freeze
|
|
44
|
+
|
|
45
|
+
attr_reader :package_path, :layers, :dependency_views
|
|
46
|
+
|
|
47
|
+
def initialize(package_path, storage:, manifest:, dependency_views: [],
|
|
48
|
+
exported_only: false)
|
|
49
|
+
@package_path = package_path
|
|
50
|
+
@storage = storage
|
|
51
|
+
@manifest = manifest
|
|
52
|
+
@exported_only = exported_only
|
|
53
|
+
@dependency_views = dependency_views
|
|
54
|
+
@layers = build_layers
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The absolute filesystem path serving a resource reference: a
|
|
58
|
+
# package-relative content path ("content/app.js"), or a dependency
|
|
59
|
+
# reference ("<dependency-guid>/content/app.js"). nil when no layer
|
|
60
|
+
# provides the path or it is tombstoned. Non-content paths never
|
|
61
|
+
# resolve through the view.
|
|
62
|
+
def resolve(reference)
|
|
63
|
+
return unless reference.is_a?(String)
|
|
64
|
+
|
|
65
|
+
if reference.include?("://")
|
|
66
|
+
pair = dependency_pair_for(reference)
|
|
67
|
+
return unless pair
|
|
68
|
+
|
|
69
|
+
return pair[1].resolve(reference.delete_prefix("#{pair[0]}/"))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
content_relative = content_relative(reference)
|
|
73
|
+
return unless content_relative
|
|
74
|
+
return if content_relative == TOMBSTONE_FILE
|
|
75
|
+
|
|
76
|
+
result = resolve_own(content_relative)
|
|
77
|
+
return if result == :tombstoned
|
|
78
|
+
return result if result
|
|
79
|
+
|
|
80
|
+
resolve_dependencies(reference)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The [GUID, view] pair whose GUID prefixes the reference (longest
|
|
84
|
+
# GUID first, so nested GUIDs address the innermost dependency).
|
|
85
|
+
def dependency_pair_for(reference)
|
|
86
|
+
@dependency_views
|
|
87
|
+
.sort_by { |guid, _view| -guid.length }
|
|
88
|
+
.find { |guid, _view| reference.start_with?("#{guid}/") }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# The serving path from the package's own layers, :tombstoned when
|
|
94
|
+
# a tombstone at or above the first hit suppresses it (tombstones
|
|
95
|
+
# also suppress the lower dependency layers), or nil when no own
|
|
96
|
+
# layer provides the path.
|
|
97
|
+
def resolve_own(content_relative)
|
|
98
|
+
tombstoned = false
|
|
99
|
+
@layers.reverse_each do |layer|
|
|
100
|
+
tombstoned ||= layer.tombstones.include?(content_relative)
|
|
101
|
+
next unless layer.file?(content_relative)
|
|
102
|
+
return :tombstoned if tombstoned
|
|
103
|
+
|
|
104
|
+
return layer.absolute(content_relative)
|
|
105
|
+
end
|
|
106
|
+
tombstoned ? :tombstoned : nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def resolve_dependencies(relative_path)
|
|
110
|
+
@dependency_views.each do |pair|
|
|
111
|
+
found = pair.last.resolve(relative_path)
|
|
112
|
+
return found if found
|
|
113
|
+
end
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Own layers, bottom -> top: the implicit content/ layer plus the
|
|
118
|
+
# configured storage.layers. Private layers are dropped when the view
|
|
119
|
+
# is exported-only (hidden from dependents, section 5a).
|
|
120
|
+
def build_layers
|
|
121
|
+
configs = [nil] + @storage.config.layers
|
|
122
|
+
configs = configs.reject { |config| config&.private? } if @exported_only
|
|
123
|
+
configs.filter_map { |config| build_layer(config) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def build_layer(config)
|
|
127
|
+
root = config ? File.join(@package_path, config.path) : content_root
|
|
128
|
+
Layer.new(root: root,
|
|
129
|
+
visibility: config&.visibility || "exported",
|
|
130
|
+
tombstones: load_tombstones(root))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def content_root
|
|
134
|
+
File.join(@package_path, Package::CONTENT_DIR)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def load_tombstones(root)
|
|
138
|
+
tombstone_path = File.join(root, TOMBSTONE_FILE)
|
|
139
|
+
return Set.new unless File.file?(tombstone_path)
|
|
140
|
+
|
|
141
|
+
entries = JSON.parse(File.read(tombstone_path))
|
|
142
|
+
unless entries.is_a?(Array) && entries.all?(String)
|
|
143
|
+
raise Error, "Malformed #{TOMBSTONE_FILE} (expected a JSON array " \
|
|
144
|
+
"of paths): #{tombstone_path}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
Set.new(entries)
|
|
148
|
+
rescue JSON::ParserError => e
|
|
149
|
+
raise Error, "Malformed #{TOMBSTONE_FILE}: #{tombstone_path}: #{e.message}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def content_relative(relative_path)
|
|
153
|
+
return unless relative_path.is_a?(String)
|
|
154
|
+
return unless relative_path.start_with?(CONTENT_PREFIX)
|
|
155
|
+
|
|
156
|
+
content_relative = relative_path.delete_prefix(CONTENT_PREFIX)
|
|
157
|
+
return if @exported_only && !exported?(relative_path)
|
|
158
|
+
|
|
159
|
+
content_relative
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def exported?(relative_path)
|
|
163
|
+
resource = @manifest.lookup(relative_path)
|
|
164
|
+
resource&.visibility == "exported"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# lib/capsium/package/metadata.rb
|
|
4
|
-
require "shale"
|
|
5
3
|
require "forwardable"
|
|
6
|
-
require_relative "metadata_config"
|
|
7
4
|
|
|
8
5
|
module Capsium
|
|
9
6
|
class Package
|
|
@@ -11,11 +8,10 @@ module Capsium
|
|
|
11
8
|
attr_reader :path, :config
|
|
12
9
|
|
|
13
10
|
extend Forwardable
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def_delegator :@config, :dependencies
|
|
11
|
+
|
|
12
|
+
def_delegators :@config, :to_json, :to_hash, :name, :version,
|
|
13
|
+
:description, :guid, :uuid, :author, :license,
|
|
14
|
+
:repository, :dependencies
|
|
19
15
|
|
|
20
16
|
def initialize(path)
|
|
21
17
|
@path = path
|
|
@@ -1,17 +1,105 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "lutaml/model"
|
|
5
|
+
require "uri"
|
|
2
6
|
|
|
3
7
|
module Capsium
|
|
4
8
|
class Package
|
|
5
|
-
class
|
|
6
|
-
attribute :
|
|
7
|
-
attribute :
|
|
9
|
+
class Repository < Lutaml::Model::Serializable
|
|
10
|
+
attribute :type, :string
|
|
11
|
+
attribute :url, :string
|
|
12
|
+
|
|
13
|
+
json do
|
|
14
|
+
map :type, to: :type
|
|
15
|
+
map :url, to: :url
|
|
16
|
+
end
|
|
8
17
|
end
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
# Canonical metadata.json model (ARCHITECTURE.md section 2).
|
|
20
|
+
#
|
|
21
|
+
# The legacy gem form of "dependencies" (an array of {name, version}
|
|
22
|
+
# objects) is accepted on read and normalized to the canonical object
|
|
23
|
+
# form; writers emit only the object form.
|
|
24
|
+
class MetadataData < Lutaml::Model::Serializable
|
|
25
|
+
KEBAB_CASE_PATTERN = /\A[a-z0-9]+(-[a-z0-9]+)*\z/
|
|
26
|
+
SEMVER_PATTERN = /\A\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\z/
|
|
27
|
+
UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
28
|
+
|
|
29
|
+
attribute :name, :string
|
|
30
|
+
attribute :version, :string
|
|
31
|
+
attribute :description, :string
|
|
32
|
+
attribute :guid, :string
|
|
33
|
+
attribute :uuid, :string
|
|
34
|
+
attribute :author, :string
|
|
35
|
+
attribute :license, :string
|
|
36
|
+
attribute :repository, Repository
|
|
37
|
+
attribute :dependencies, :hash, default: {}
|
|
38
|
+
attribute :read_only, :boolean
|
|
39
|
+
|
|
40
|
+
json do
|
|
41
|
+
map :name, to: :name
|
|
42
|
+
map :version, to: :version
|
|
43
|
+
map :description, to: :description
|
|
44
|
+
map :guid, to: :guid
|
|
45
|
+
map :uuid, to: :uuid
|
|
46
|
+
map :author, to: :author
|
|
47
|
+
map :license, to: :license
|
|
48
|
+
map :repository, to: :repository
|
|
49
|
+
map :dependencies, to: :dependencies
|
|
50
|
+
map "readOnly", to: :read_only
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.from_json(json)
|
|
54
|
+
doc = JSON.parse(json)
|
|
55
|
+
doc["dependencies"] = normalize_dependencies(doc["dependencies"])
|
|
56
|
+
super(JSON.generate(doc))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.normalize_dependencies(dependencies)
|
|
60
|
+
return {} if dependencies.nil?
|
|
61
|
+
return dependencies unless dependencies.is_a?(Array)
|
|
62
|
+
|
|
63
|
+
dependencies.to_h { |dep| [dep["name"], dep["version"]] }
|
|
64
|
+
end
|
|
65
|
+
private_class_method :normalize_dependencies
|
|
66
|
+
|
|
67
|
+
# Field-level format validations (ARCHITECTURE.md section 2).
|
|
68
|
+
# Returns a list of human-readable problems; empty when valid.
|
|
69
|
+
def format_errors
|
|
70
|
+
presence_errors + format_field_errors
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def presence_errors
|
|
76
|
+
problems = []
|
|
77
|
+
problems << "name is missing" if name.to_s.empty?
|
|
78
|
+
problems << "version is missing" if version.to_s.empty?
|
|
79
|
+
problems << "description is missing" if description.to_s.empty?
|
|
80
|
+
problems << "guid is missing" if guid.to_s.empty?
|
|
81
|
+
problems << "uuid is missing" if uuid.to_s.empty?
|
|
82
|
+
problems
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def format_field_errors
|
|
86
|
+
problems = []
|
|
87
|
+
problems << "name must be kebab-case" if invalid?(name, KEBAB_CASE_PATTERN)
|
|
88
|
+
problems << "version must be semver" if invalid?(version, SEMVER_PATTERN)
|
|
89
|
+
problems << "guid must be a URI" if guid && !uri?(guid)
|
|
90
|
+
problems << "uuid is not a valid UUID" if invalid?(uuid, UUID_PATTERN)
|
|
91
|
+
problems
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def invalid?(value, pattern)
|
|
95
|
+
!value.nil? && !value.match?(pattern)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def uri?(value)
|
|
99
|
+
URI.parse(value).is_a?(URI::Generic)
|
|
100
|
+
rescue URI::InvalidURIError
|
|
101
|
+
false
|
|
102
|
+
end
|
|
15
103
|
end
|
|
16
104
|
end
|
|
17
105
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
module Capsium
|
|
7
|
+
class Package
|
|
8
|
+
# Package-source preparation (ARCHITECTURE.md section 1), mixed into
|
|
9
|
+
# Package: directory/.cap/encrypted-cap detection, extraction and
|
|
10
|
+
# decryption into a readable package directory.
|
|
11
|
+
module Preparation
|
|
12
|
+
def prepare_package(path)
|
|
13
|
+
return decrypt_cap_file(path) if Cipher.encrypted?(path)
|
|
14
|
+
return path if File.directory?(path)
|
|
15
|
+
raise Error, "Invalid package path: #{path}" unless File.file?(path)
|
|
16
|
+
raise Error, "The package must have a .cap extension" unless File.extname(path) == ".cap"
|
|
17
|
+
|
|
18
|
+
decompress_cap_file(path)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def decompress_cap_file(file_path)
|
|
22
|
+
package_path = File.join(Dir.mktmpdir, package_stem(file_path))
|
|
23
|
+
FileUtils.mkdir_p(package_path)
|
|
24
|
+
Packager.new.unpack(file_path, package_path)
|
|
25
|
+
package_path
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Decrypts an encrypted package (.cap file or uncompressed directory)
|
|
29
|
+
# into a temporary directory and returns its path. The metadata.json
|
|
30
|
+
# of an encrypted package stays cleartext, but everything else is
|
|
31
|
+
# only readable with the recipient's private key.
|
|
32
|
+
def decrypt_cap_file(source_path)
|
|
33
|
+
unless @decryption_key
|
|
34
|
+
raise Cipher::KeyRequiredError,
|
|
35
|
+
"Package is encrypted; provide the private key via decryption_key:"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Cipher.decrypt_to_directory(source_path, @decryption_key)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def determine_load_type(path)
|
|
42
|
+
return :directory if File.directory?(path)
|
|
43
|
+
|
|
44
|
+
File.extname(path) == ".cap" ? :cap_file : :unsaved
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Whether the package was loaded from an encrypted source
|
|
48
|
+
# (ARCHITECTURE.md section 6b layout).
|
|
49
|
+
def encrypted?
|
|
50
|
+
Cipher.encrypted?(@original_path)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def package_stem(file_path)
|
|
56
|
+
File.basename(file_path, ".cap")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def create_package_structure
|
|
60
|
+
FileUtils.mkdir_p(content_path)
|
|
61
|
+
FileUtils.mkdir_p(data_path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def content_path = File.join(@path, CONTENT_DIR)
|
|
65
|
+
|
|
66
|
+
def data_path = File.join(@path, DATA_DIR)
|
|
67
|
+
|
|
68
|
+
def routes_path = File.join(@path, ROUTES_FILE)
|
|
69
|
+
|
|
70
|
+
def storage_path = File.join(@path, STORAGE_FILE)
|
|
71
|
+
|
|
72
|
+
def metadata_path = File.join(@path, METADATA_FILE)
|
|
73
|
+
|
|
74
|
+
def manifest_path = File.join(@path, MANIFEST_FILE)
|
|
75
|
+
|
|
76
|
+
def security_path = File.join(@path, SECURITY_FILE)
|
|
77
|
+
|
|
78
|
+
def authentication_path = File.join(@path, AUTHENTICATION_FILE)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "json"
|
|
5
|
-
require "fileutils"
|
|
6
|
-
require_relative "routes_config"
|
|
3
|
+
require "forwardable"
|
|
7
4
|
|
|
8
5
|
module Capsium
|
|
9
6
|
class Package
|
|
10
7
|
class Routes
|
|
11
8
|
extend Forwardable
|
|
12
|
-
attr_reader :config
|
|
13
9
|
|
|
14
|
-
|
|
10
|
+
attr_reader :path, :config, :manifest, :storage
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
def_delegators :@config, :to_hash
|
|
17
13
|
|
|
18
|
-
ROUTES_FILE = "routes.json"
|
|
19
|
-
DEFAULT_INDEX_TARGET = "content/index.html"
|
|
20
14
|
INDEX_ROUTE = "/"
|
|
15
|
+
INDEX_RESOURCE = "content/index.html"
|
|
16
|
+
DATASET_ROUTE_PREFIX = "/api/v1/data/"
|
|
21
17
|
|
|
22
18
|
def initialize(path, manifest, storage)
|
|
23
19
|
@path = path
|
|
24
|
-
@dir = File.dirname(path)
|
|
25
20
|
@manifest = manifest
|
|
26
21
|
@storage = storage
|
|
27
22
|
@config = if File.exist?(path)
|
|
@@ -29,8 +24,6 @@ module Capsium
|
|
|
29
24
|
else
|
|
30
25
|
generate_routes
|
|
31
26
|
end
|
|
32
|
-
validate_index_path(@config.resolve(INDEX_ROUTE)&.target&.file)
|
|
33
|
-
validate
|
|
34
27
|
end
|
|
35
28
|
|
|
36
29
|
def resolve(url_path)
|
|
@@ -38,12 +31,10 @@ module Capsium
|
|
|
38
31
|
end
|
|
39
32
|
|
|
40
33
|
def add_route(route, target)
|
|
41
|
-
validate_route_target(route, target)
|
|
42
34
|
@config.add(route, target)
|
|
43
35
|
end
|
|
44
36
|
|
|
45
37
|
def update_route(route, updated_route, updated_target)
|
|
46
|
-
validate_route_target(updated_route, updated_target)
|
|
47
38
|
@config.update(route, updated_route, updated_target)
|
|
48
39
|
end
|
|
49
40
|
|
|
@@ -51,48 +42,55 @@ module Capsium
|
|
|
51
42
|
@config.remove(route)
|
|
52
43
|
end
|
|
53
44
|
|
|
54
|
-
def validate
|
|
55
|
-
@config.routes.each do |route|
|
|
56
|
-
route.target.validate(@manifest, @storage)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
45
|
def to_json(*_args)
|
|
61
|
-
@config.
|
|
46
|
+
@config.to_json
|
|
62
47
|
end
|
|
63
48
|
|
|
64
49
|
def save_to_file(output_path = @path)
|
|
65
50
|
File.write(output_path, to_json)
|
|
66
51
|
end
|
|
67
52
|
|
|
53
|
+
# Auto-generation (ARCHITECTURE.md section 4): every manifest
|
|
54
|
+
# resource gets a route at its path relative to content/; HTML files
|
|
55
|
+
# get two routes (basename without extension and full filename); the
|
|
56
|
+
# index HTML additionally gets "/"; every dataset in storage gets
|
|
57
|
+
# /api/v1/data/<id>. Output is deterministic (sorted by path).
|
|
58
|
+
def generate_routes
|
|
59
|
+
routes = resource_routes + dataset_routes
|
|
60
|
+
RoutesConfig.new(index: index_resource, routes: routes.sort_by(&:path))
|
|
61
|
+
end
|
|
62
|
+
|
|
68
63
|
private
|
|
69
64
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
relative_path = data_item.file.sub(/^#{Package::CONTENT_DIR}/o, "")
|
|
74
|
-
r.add(relative_path, data_item.file)
|
|
75
|
-
|
|
76
|
-
# Ensure the index route is included
|
|
77
|
-
if File.basename(relative_path, ".*") == "index"
|
|
78
|
-
r.add("/index", data_item.file)
|
|
79
|
-
r.add("/", data_item.file)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
65
|
+
def index_resource
|
|
66
|
+
INDEX_RESOURCE if @manifest.resources.key?(INDEX_RESOURCE)
|
|
67
|
+
end
|
|
82
68
|
|
|
83
|
-
|
|
69
|
+
def resource_routes
|
|
70
|
+
@manifest.resources.keys.flat_map do |resource_path|
|
|
71
|
+
routes_for_resource(resource_path)
|
|
72
|
+
end.uniq(&:path)
|
|
84
73
|
end
|
|
85
74
|
|
|
86
|
-
def
|
|
87
|
-
|
|
75
|
+
def routes_for_resource(resource_path)
|
|
76
|
+
url_path = resource_path.sub(%r{\A#{Package::CONTENT_DIR}/}o, "")
|
|
77
|
+
routes = [Route.new(path: "/#{url_path}", resource: resource_path)]
|
|
78
|
+
return routes unless File.extname(resource_path) == ".html"
|
|
88
79
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
basename = File.basename(url_path, ".html")
|
|
81
|
+
routes << Route.new(path: "/#{basename}", resource: resource_path)
|
|
82
|
+
routes << Route.new(path: INDEX_ROUTE, resource: resource_path) if index?(resource_path)
|
|
83
|
+
routes
|
|
92
84
|
end
|
|
93
85
|
|
|
94
|
-
def
|
|
95
|
-
|
|
86
|
+
def index?(resource_path)
|
|
87
|
+
resource_path == INDEX_RESOURCE
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def dataset_routes
|
|
91
|
+
@storage.dataset_names.map do |name|
|
|
92
|
+
Route.new(path: "#{DATASET_ROUTE_PREFIX}#{name}", dataset: name)
|
|
93
|
+
end
|
|
96
94
|
end
|
|
97
95
|
end
|
|
98
96
|
end
|