capsium 0.1.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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rake.yml +18 -0
  3. data/.github/workflows/release.yml +24 -0
  4. data/.gitignore +11 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +8 -0
  7. data/CODE_OF_CONDUCT.md +84 -0
  8. data/Gemfile +12 -0
  9. data/README.adoc +258 -0
  10. data/Rakefile +12 -0
  11. data/bin/console +11 -0
  12. data/bin/setup +8 -0
  13. data/capsium.gemspec +54 -0
  14. data/exe/capsium +6 -0
  15. data/images/005df7415a331c466ad2d9a42efdd212b6f42fe50b5fd3b3174c86c706f58244.png +0 -0
  16. data/images/0374025b3af99b8a473282c8cbbf9fcd29573cf41e586982f328f86c0690f43d.png +0 -0
  17. data/images/0bb4da785be40ef58e219470ebccb979325928b75453dc46bac23c6ee8a7a7cb.png +0 -0
  18. data/images/6aa294dccc81af594aacbe804e7ddffdc17eacc28357338108aea5d021d831ff.png +0 -0
  19. data/images/72dd3fbf3f4b475e27a0e7fb8137c475c32c41f8d222bcf62d6a9ccf102d9532.png +0 -0
  20. data/images/8772b6961d169738d7b0fa0b669b06fc2f40632d4c62586c7634fc17b93182a3.png +0 -0
  21. data/images/a998d842405933d45723606ff3f70162ec95b4ef30db25464a366184fd08fb9b.png +0 -0
  22. data/images/aa8980547e8c003d33273ab4d80e62da7f317bd7581b293c06d67f5331f24f31.png +0 -0
  23. data/images/bb78a872b539e0e9b2d80dee58acbb688f3f2727b324a5bf8bf417a69d94a166.png +0 -0
  24. data/images/c48fc83b17725d85fbb64d971196ebfccd8c5c757fe6aa5845303f6e315879b6.png +0 -0
  25. data/images/f08ef07308d08119ac2124bb7428c8bef17ef1ca70045696604d6e83015a9b91.png +0 -0
  26. data/images/f7514206111b695647eae9adfcf498ba3e0ff83ecfe25f3fc3ed8e9f04c5c726-1.png +0 -0
  27. data/images/f7514206111b695647eae9adfcf498ba3e0ff83ecfe25f3fc3ed8e9f04c5c726.png +0 -0
  28. data/lib/capsium/cli.rb +96 -0
  29. data/lib/capsium/converters/jekyll.rb +59 -0
  30. data/lib/capsium/package/dataset.rb +86 -0
  31. data/lib/capsium/package/manifest.rb +97 -0
  32. data/lib/capsium/package/metadata.rb +48 -0
  33. data/lib/capsium/package/routes.rb +157 -0
  34. data/lib/capsium/package/storage.rb +52 -0
  35. data/lib/capsium/package.rb +142 -0
  36. data/lib/capsium/packager.rb +100 -0
  37. data/lib/capsium/protector.rb +95 -0
  38. data/lib/capsium/reactor.rb +82 -0
  39. data/lib/capsium/thor_ext.rb +71 -0
  40. data/lib/capsium/version.rb +5 -0
  41. data/lib/capsium.rb +15 -0
  42. data/sig/capsium.rbs +4 -0
  43. metadata +354 -0
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/capsium/packager.rb
4
+ require "json"
5
+ require "fileutils"
6
+ require "rubygems"
7
+ require "zip"
8
+
9
+ module Capsium
10
+ class Packager
11
+ class FileAlreadyExistsError < StandardError; end
12
+
13
+ def pack(package, options = {})
14
+ package_name = package.metadata.name
15
+ package_version = package.metadata.version
16
+ cap_file_path = File.expand_path(
17
+ File.join(package.path, "..", "#{package_name}-#{package_version}.cap")
18
+ )
19
+
20
+ if File.exist?(cap_file_path)
21
+ unless options[:force]
22
+ raise FileAlreadyExistsError,
23
+ "Package target already exists, aborting: `#{relative_path_current(cap_file_path)}`"
24
+ end
25
+
26
+ puts "Package target already exists, overwriting: `#{relative_path_current(cap_file_path)}`"
27
+ FileUtils.rm_f(cap_file_path)
28
+ end
29
+
30
+ Dir.mktmpdir do |dir|
31
+ FileUtils.cp_r("#{package.path}/.", dir)
32
+ new_package = Package.new(dir)
33
+ new_package.solidify
34
+ compress_package(new_package, cap_file_path)
35
+ puts "Package created: #{relative_path_current(cap_file_path)}"
36
+ end
37
+ end
38
+
39
+ def compress_package(package, cap_file)
40
+ Zip::File.open(cap_file, Zip::File::CREATE) do |zipfile|
41
+ Dir[File.join(package.path, "**", "**")].each do |file|
42
+ zipfile.add(file.sub("#{package.path}/", ""), file)
43
+ end
44
+ end
45
+ end
46
+
47
+ # def package_files
48
+ # create_metadata_file
49
+ # create_manifest_file
50
+ # create_packaging_file
51
+
52
+ # compressor = Compressor.new(@package, @package.metadata[:compression])
53
+ # compressor.compress
54
+
55
+ # protector = Protector.new(@package, @package.metadata[:encryption], @package.metadata[:signature])
56
+ # protector.apply_encryption_and_sign
57
+ # end
58
+
59
+ # private
60
+
61
+ # def create_metadata_file
62
+ # metadata_path = File.join(@package.path, Package::METADATA_FILE)
63
+ # metadata_content = @package.metadata.to_h
64
+ # write_json_file(metadata_path, metadata_content)
65
+ # end
66
+
67
+ # def create_manifest_file
68
+ # manifest_path = File.join(@package.path, Package::MANIFEST_FILE)
69
+ # manifest_content = {
70
+ # files: Dir[File.join(@package.path, '**', '*')].reject { |f| File.directory?(f) }
71
+ # }
72
+ # write_json_file(manifest_path, manifest_content)
73
+ # end
74
+
75
+ def create_packaging_file
76
+ packaging_path = File.join(@package.path, Package::PACKAGING_FILE)
77
+ packaging_content = {
78
+ name: @package.name,
79
+ content_path: relative_path_package(@package.content_path),
80
+ data_path: relative_path_package(@package.data_path),
81
+ datasets: @package.datasets.map(&:to_h)
82
+ }
83
+ write_json_file(packaging_path, packaging_content)
84
+ end
85
+
86
+ def write_json_file(path, content)
87
+ File.open(path, "w") do |file|
88
+ file.write(JSON.pretty_generate(content))
89
+ end
90
+ end
91
+
92
+ def relative_path_package(absolute_path)
93
+ Pathname.new(absolute_path).relative_path_from(Pathname.new(@package.path)).to_s
94
+ end
95
+
96
+ def relative_path_current(absolute_path)
97
+ Pathname.new(absolute_path).relative_path_from(Dir.pwd).to_s
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/capsium/protector.rb
4
+ require "openssl"
5
+ require "base64"
6
+ require "json"
7
+
8
+ module Capsium
9
+ class Protector
10
+ def initialize(package, encryption_metadata = nil, digital_signature_metadata = nil)
11
+ @package = package
12
+ @encryption_metadata = encryption_metadata
13
+ @digital_signature_metadata = digital_signature_metadata
14
+ end
15
+
16
+ def apply_encryption_and_sign
17
+ encrypted_file = apply_encryption if @encryption_metadata
18
+ sign_package(encrypted_file) if @digital_signature_metadata
19
+ end
20
+
21
+ def verify_signature
22
+ signature_data = JSON.parse(File.read(signature_file_path))
23
+ public_key = OpenSSL::PKey::RSA.new(File.read(public_key_path))
24
+ digest = OpenSSL::Digest.new(signature_data["algorithm"])
25
+ data = combined_data
26
+ signature = Base64.decode64(signature_data["signature"])
27
+
28
+ public_key.verify(digest, signature, data)
29
+ end
30
+
31
+ private
32
+
33
+ def apply_encryption
34
+ encryption = @encryption_metadata
35
+ data = File.read(File.join(@package.path, Package::ENCRYPTED_PACKAGING_FILE))
36
+ cipher = OpenSSL::Cipher.new(encryption[:algorithm])
37
+ cipher.encrypt
38
+ key = cipher.random_key
39
+ iv = cipher.random_iv
40
+
41
+ encrypted_data = cipher.update(data) + cipher.final
42
+ encrypted_file = File.join(@package.path, "#{@package.name}.enc")
43
+
44
+ File.open(encrypted_file, "wb") do |f|
45
+ f.write(iv)
46
+ f.write(encrypted_data)
47
+ end
48
+
49
+ save_encryption_key(key, encryption[:keyManagement])
50
+ encrypted_file
51
+ end
52
+
53
+ def save_encryption_key(key, key_management)
54
+ case key_management
55
+ when "secure"
56
+ # Implement secure key storage mechanism
57
+ File.write(File.join(@package.path, "encryption_key.secure"), Base64.encode64(key))
58
+ else
59
+ raise "Unknown key management strategy: #{key_management}"
60
+ end
61
+ end
62
+
63
+ def sign_package(encrypted_file)
64
+ key = OpenSSL::PKey::RSA.new(@digital_signature_metadata[:keyLength])
65
+ data = combined_data(encrypted_file)
66
+ digest = OpenSSL::Digest.new(@digital_signature_metadata[:algorithm])
67
+ signature_data = key.sign(digest, data)
68
+
69
+ signature_json = {
70
+ algorithm: @digital_signature_metadata[:algorithm],
71
+ certificateType: @digital_signature_metadata[:certificateType],
72
+ signature: Base64.encode64(signature_data)
73
+ }
74
+
75
+ File.write(signature_file_path, JSON.pretty_generate(signature_json))
76
+ File.write(public_key_path, key.public_key.to_pem)
77
+ end
78
+
79
+ def combined_data(encrypted_file = nil)
80
+ metadata_content = File.read(File.join(@package.path, Package::METADATA_FILE))
81
+ signature_content = File.read(signature_file_path).sub(/"signature": ".*"/, '"signature": ""')
82
+ package_enc_content = File.read(encrypted_file || File.join(@package.path, "#{@package.name}.enc"))
83
+
84
+ metadata_content + signature_content + package_enc_content
85
+ end
86
+
87
+ def signature_file_path
88
+ File.join(@package.path, @digital_signature_metadata[:signatureFile])
89
+ end
90
+
91
+ def public_key_path
92
+ File.join(@package.path, "public_key.pem")
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/capsium/reactor.rb
4
+ require "webrick"
5
+ require "json"
6
+ require "listen"
7
+ require "capsium/package"
8
+
9
+ module Capsium
10
+ class Reactor
11
+ DEFAULT_PORT = 8864
12
+ attr_accessor :package, :package_path, :routes, :port, :server, :server_thread
13
+
14
+ def initialize(package, port = DEFAULT_PORT)
15
+ @package = package
16
+ @package_path = package.path
17
+ @port = port
18
+ @server = WEBrick::HTTPServer.new(Port: @port)
19
+ load_routes
20
+ mount_routes
21
+ end
22
+
23
+ def serve
24
+ @server_thread = start_server
25
+ start_listener
26
+ end
27
+
28
+ private
29
+
30
+ def load_routes
31
+ @routes = @package.routes.data.routes
32
+ end
33
+
34
+ def mount_routes
35
+ @routes.each do |route|
36
+ path = route.path
37
+ target = route.target
38
+ content_path = target.fs_path(@package.manifest)
39
+ puts "mounting route: #{path} => #{content_path}"
40
+ @server.mount_proc(path.to_s) do |_req, res|
41
+ res.body = File.read(content_path)
42
+ res.content_type = target.mime(@package.manifest)
43
+ end
44
+ end
45
+ end
46
+
47
+ def start_server
48
+ @server_thread = Thread.new do
49
+ trap("INT") do
50
+ puts "\nShutting down server..."
51
+ @server.shutdown
52
+ exit
53
+ end
54
+
55
+ puts "Starting server on http://localhost:#{@port}"
56
+ @server.start
57
+ end
58
+ end
59
+
60
+ def restart_server
61
+ @server.shutdown
62
+ # @server_thread.kill
63
+ load_package
64
+ @server = WEBrick::HTTPServer.new(Port: @port)
65
+ mount_routes
66
+ start_server
67
+ end
68
+
69
+ def start_listener
70
+ listener = Listen.to(@package_path) do |_modified, _added, _removed|
71
+ puts "Changes detected, reloading..."
72
+ restart_server
73
+ end
74
+
75
+ listener.start
76
+ puts "Listening for changes in #{@package_path}..."
77
+
78
+ # Wait for the server thread to finish
79
+ @server_thread.join
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsium
4
+ module ThorExt
5
+ # Configures Thor to behave more like a typical CLI, with better help and error handling.
6
+ #
7
+ # - Passing -h or --help to a command will show help for that command.
8
+ # - Unrecognized options will be treated as errors (instead of being silently ignored).
9
+ # - Error messages will be printed in red to stderr, without stack trace.
10
+ # - Full stack traces can be enabled by setting the VERBOSE environment variable.
11
+ # - Errors will cause Thor to exit with a non-zero status.
12
+ #
13
+ # To take advantage of this behavior, your CLI should subclass Thor and extend this module.
14
+ #
15
+ # class CLI < Thor
16
+ # extend ThorExt::Start
17
+ # end
18
+ #
19
+ # Start your CLI with:
20
+ #
21
+ # CLI.start
22
+ #
23
+ # In tests, prevent Kernel.exit from being called when an error occurs, like this:
24
+ #
25
+ # CLI.start(args, exit_on_failure: false)
26
+ #
27
+ module Start
28
+ def self.extended(base)
29
+ super
30
+ base.check_unknown_options!
31
+ end
32
+
33
+ def start(given_args = ARGV, config = {})
34
+ config[:shell] ||= Thor::Base.shell.new
35
+ handle_help_switches(given_args) do |args|
36
+ dispatch(nil, args, nil, config)
37
+ end
38
+ rescue StandardError => e
39
+ handle_exception_on_start(e, config)
40
+ end
41
+
42
+ private
43
+
44
+ def handle_help_switches(given_args)
45
+ yield(given_args.dup)
46
+ rescue Thor::UnknownArgumentError => e
47
+ retry_with_args = []
48
+
49
+ if given_args.first == "help"
50
+ retry_with_args = ["help"] if given_args.length > 1
51
+ elsif e.unknown.intersect?(%w[-h --help])
52
+ retry_with_args = ["help", (given_args - e.unknown).first]
53
+ end
54
+ raise unless retry_with_args.any?
55
+
56
+ yield(retry_with_args)
57
+ end
58
+
59
+ def handle_exception_on_start(error, config)
60
+ return if error.is_a?(Errno::EPIPE)
61
+ raise if ENV["VERBOSE"] || !config.fetch(:exit_on_failure, true)
62
+
63
+ message = error.message.to_s
64
+ message.prepend("[#{error.class}] ") if message.empty? || !error.is_a?(Thor::Error)
65
+
66
+ config[:shell]&.say_error(message, :red)
67
+ exit(false)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capsium
4
+ VERSION = "0.1.1"
5
+ end
data/lib/capsium.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "capsium/version"
4
+ require "shale"
5
+ require "shale/adapter/nokogiri"
6
+ Shale.xml_adapter = Shale::Adapter::Nokogiri
7
+
8
+ module Capsium
9
+ class Error < StandardError; end
10
+
11
+ # Your code goes here...
12
+ end
13
+
14
+ require_relative "capsium/package"
15
+ require_relative "capsium/packager"
data/sig/capsium.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Capsium
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end