gotenberg 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d984cff5f3354d5dd5ab3a8eb7210fa49532125cf9d8d7ef57a729243d3434aa
4
+ data.tar.gz: c1f52733269daac044db7c1930d0b276924484aaa365a2919719fa2e55e70c98
5
+ SHA512:
6
+ metadata.gz: 3a86064442da347abc5a5bc1aebdced0921935f545bc300d49ef69ad63c33f86a4f24a129dfc158aec5ffda08a9de331f4bb77fc218017b88e7602e8662bd080
7
+ data.tar.gz: ef1dd7bc42f1763bf4b1a1d22c5cf5937cc924db422c56d353dc7c92be48683ea0d531b332ff9cfbf88391e641d0006dec06b2c7c91ddda1ea5d6689c7bfbe5b
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "mime/types"
7
+ require "net/http/post/multipart"
8
+ require "tempfile"
9
+ require "securerandom"
10
+ require "fileutils"
11
+
12
+ module Gotenberg
13
+ class IndexFileMissing < StandardError; end
14
+
15
+ # Client class for interacting with the Gotenberg API
16
+ class Client
17
+ # Initialize a new Client
18
+ #
19
+ # @param api_url [String] The base URL of the Gotenberg API
20
+ def initialize(api_url)
21
+ @api_url = api_url
22
+ end
23
+
24
+ # Convert HTML files to PDF and write it to the output file
25
+ #
26
+ # @param htmls [Hash{Symbol => String}] A hash with the file name as the key and the HTML content as the value
27
+ # @param asset_paths [Array<String>] Paths to the asset files (like CSS, images) required by the HTML files
28
+ # @param properties [Hash] Additional properties for PDF conversion
29
+ # @option properties [Float] :paperWidth The width of the paper
30
+ # @option properties [Float] :paperHeight The height of the paper
31
+ # @option properties [Float] :marginTop The top margin
32
+ # @option properties [Float] :marginBottom The bottom margin
33
+ # @option properties [Float] :marginLeft The left margin
34
+ # @option properties [Float] :marginRight The right margin
35
+ # @option properties [Boolean] :preferCssPageSize Whether to prefer CSS page size
36
+ # @option properties [Boolean] :printBackground Whether to print the background
37
+ # @option properties [Boolean] :omitBackground Whether to omit the background
38
+ # @option properties [Boolean] :landscape Whether to use landscape orientation
39
+ # @option properties [Float] :scale The scale of the PDF
40
+ # @option properties [String] :nativePageRanges The page ranges to include
41
+ # @return [String] The resulting PDF content
42
+ # @raise [GotenbergDownError] if the Gotenberg API is down
43
+ #
44
+ # Example:
45
+ # htmls = { index: "<h1>Html</h1>", header: "<h1>Header</h1>", footer: "<h1>Footer</h1>" }
46
+ # asset_paths = ["path/to/style.css", "path/to/image.png"]
47
+ # properties = { paperWidth: 8.27, paperHeight: 11.7, marginTop: 1, marginBottom: 1 }
48
+ # client = Gotenberg::Client.new("http://localhost:3000")
49
+ # pdf_content = client.html(htmls, asset_paths, properties)
50
+ #
51
+ def html(htmls, asset_paths, properties = {}) # rubocop:disable Metrics/CyclomaticComplexity
52
+ raise GotenbergDownError unless up?
53
+
54
+ raise IndexFileMissing unless (htmls.keys & ["index", :index]).any?
55
+
56
+ dir_name = SecureRandom.uuid
57
+ dir_path = File.join(Dir.tmpdir, dir_name)
58
+ FileUtils.mkdir_p(dir_path)
59
+
60
+ htmls.each do |key, value|
61
+ File.write(File.join(dir_path, "#{key}.html"), value)
62
+ end
63
+
64
+ uri = URI("#{@api_url}/forms/chromium/convert/html")
65
+
66
+ # Gotenberg requires all files to be in the same directory
67
+ asset_paths.each do |path|
68
+ FileUtils.cp(path, dir_path)
69
+ end
70
+
71
+ # Rejecting .. and .
72
+ entries = Dir.entries(dir_path).reject { |f| f.start_with?(".") }
73
+
74
+ payload = entries.each_with_object({}).with_index do |(entry, obj), index|
75
+ entry_abs_path = File.join(dir_path, entry)
76
+ mime_type = MIME::Types.type_for(entry_abs_path).first.content_type
77
+ obj["files[#{index}]"] = UploadIO.new(entry_abs_path, mime_type)
78
+ end
79
+
80
+ response = multipart_post(uri, payload.merge(properties))
81
+ response.body.dup.force_encoding("utf-8")
82
+ ensure
83
+ FileUtils.rm_rf(dir_path) if dir_path
84
+ end
85
+
86
+ # Check if the Gotenberg API is up and healthy
87
+ #
88
+ # @return [Boolean] true if the API is up, false otherwise
89
+ def up?
90
+ uri = URI("#{@api_url}/health")
91
+ request = Net::HTTP::Get.new(uri)
92
+ request.basic_auth(
93
+ ENV.fetch("GOTENBERG_API_BASIC_AUTH_USERNAME", nil),
94
+ ENV.fetch("GOTENBERG_API_BASIC_AUTH_PASSWORD", nil)
95
+ )
96
+
97
+ http = Net::HTTP.new(uri.host, uri.port)
98
+ http.use_ssl = uri.scheme == "https"
99
+ response = http.request(request)
100
+
101
+ response.is_a?(Net::HTTPSuccess) && JSON.parse(response.body)["status"] == "up"
102
+ rescue StandardError
103
+ false
104
+ end
105
+
106
+ private
107
+
108
+ def multipart_post(uri, payload)
109
+ request = Net::HTTP::Post::Multipart.new(uri.path, payload)
110
+ request.basic_auth(
111
+ ENV.fetch("GOTENBERG_API_BASIC_AUTH_USERNAME", nil),
112
+ ENV.fetch("GOTENBERG_API_BASIC_AUTH_PASSWORD", nil)
113
+ )
114
+
115
+ http = Net::HTTP.new(uri.host, uri.port)
116
+ http.use_ssl = uri.scheme == "https"
117
+ http.request(request)
118
+ end
119
+ end
120
+
121
+ # Custom error class for Gotenberg API downtime
122
+ class GotenbergDownError < StandardError; end
123
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gotenberg
4
+ module Helper # rubocop:disable Style/Documentation
5
+ class ExtensionMissing < StandardError; end
6
+
7
+ class PropshaftAsset # rubocop:disable Style/Documentation
8
+ attr_reader :asset
9
+
10
+ def initialize(asset)
11
+ @asset = asset
12
+ end
13
+
14
+ def content_type
15
+ asset.content_type.to_s
16
+ end
17
+
18
+ def to_s
19
+ asset.content
20
+ end
21
+
22
+ def filename
23
+ asset.path.to_s
24
+ end
25
+ end
26
+
27
+ class MissingAsset < StandardError # rubocop:disable Style/Documentation
28
+ attr_reader :path
29
+
30
+ def initialize(path, message)
31
+ @path = path
32
+ super(message)
33
+ end
34
+ end
35
+
36
+ class LocalAsset # rubocop:disable Style/Documentation
37
+ attr_reader :path
38
+
39
+ def initialize(path)
40
+ @path = path
41
+ end
42
+
43
+ def content_type
44
+ Mime::Type.lookup_by_extension(File.extname(path).delete("."))
45
+ end
46
+
47
+ def to_s
48
+ File.read(path)
49
+ end
50
+
51
+ def filename
52
+ path.to_s
53
+ end
54
+ end
55
+
56
+ class SprocketsEnvironment # rubocop:disable Style/Documentation
57
+ def self.instance
58
+ @instance ||= Sprockets::Railtie.build_environment(Rails.application)
59
+ end
60
+
61
+ def self.find_asset(*args)
62
+ instance.find_asset(*args)
63
+ end
64
+ end
65
+
66
+ def goten_asset_base64(asset_name)
67
+ asset = find_asset(goten_static_asset_path(asset_name))
68
+ raise MissingAsset.new(asset_name, "Could not find asset '#{asset_name}'") if asset.nil?
69
+
70
+ base64 = Base64.encode64(asset.to_s).delete("\n")
71
+ "data:#{asset.content_type};base64,#{Rack::Utils.escape(base64)}"
72
+ end
73
+
74
+ def goten_static_asset_path(asset_name)
75
+ ext = File.extname(asset_name).delete(".")
76
+
77
+ raise ExtensionMissing if ext.empty?
78
+
79
+ asset_type =
80
+ case ext
81
+ when "js" then "javascripts"
82
+ when "css" then "stylesheets"
83
+ else "images"
84
+ end
85
+
86
+ determine_static_path(asset_type, asset_name)
87
+ end
88
+
89
+ def goten_compiled_asset_path(asset_name)
90
+ Rails.public_path.to_s +
91
+ ActionController::Base.helpers.asset_path(asset_name)
92
+ end
93
+
94
+ private
95
+
96
+ def determine_static_path(asset_type, asset_name)
97
+ asset_root = Rails.root.join("app", "assets")
98
+ path = asset_root.join(asset_type, asset_name)
99
+
100
+ unless File.exist?(path)
101
+ raise MissingAsset.new(
102
+ asset_name,
103
+ "Could not find static asset '#{asset_name}'"
104
+ )
105
+ end
106
+
107
+ path.to_s
108
+ end
109
+
110
+ # Thanks WickedPDF 🙏
111
+ def find_asset(path)
112
+ if Rails.application.assets.respond_to?(:find_asset)
113
+ Rails.application.assets.find_asset(path, base_path: Rails.application.root.to_s)
114
+ elsif defined?(Propshaft::Assembly) && Rails.application.assets.is_a?(Propshaft::Assembly)
115
+ PropshaftAsset.new(Rails.application.assets.load_path.find(path))
116
+ elsif Rails.application.respond_to?(:assets_manifest)
117
+ asset_path = File.join(Rails.application.assets_manifest.dir, Rails.application.assets_manifest.assets[path])
118
+ LocalAsset.new(asset_path) if File.file?(asset_path)
119
+ else
120
+ SprocketsEnvironment.find_asset(path, base_path: Rails.application.root.to_s)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "gotenberg/helper"
5
+
6
+ module Gotenberg
7
+ class Railtie < Rails::Railtie # rubocop:disable Style/Documentation
8
+ initializer "gotenberg.register" do
9
+ ActiveSupport.on_load :action_view do
10
+ include Gotenberg::Helper
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gotenberg
4
+ VERSION = "1.0.0"
5
+ end
data/lib/gotenberg.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gotenberg/version"
4
+ require_relative "gotenberg/client"
5
+ require_relative "gotenberg/railtie" if defined?(Rails::Railtie)
6
+ require_relative "gotenberg/helper"
7
+
8
+ module Gotenberg
9
+ class GotenbergDownError < StandardError; end
10
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gotenberg
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - bugloper
8
+ - teknatha136
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2024-06-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mime-types
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: multipart-post
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '2.1'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '2.1'
42
+ description: A simple Ruby client for gotenberg
43
+ email:
44
+ - bugloper@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - lib/gotenberg.rb
50
+ - lib/gotenberg/client.rb
51
+ - lib/gotenberg/helper.rb
52
+ - lib/gotenberg/railtie.rb
53
+ - lib/gotenberg/version.rb
54
+ homepage: https://github.com/SELISEdigitalplatforms/l3-ruby-gem-gotenberg
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ allowed_push_host: https://rubygems.org
59
+ homepage_uri: https://github.com/SELISEdigitalplatforms/l3-ruby-gem-gotenberg
60
+ source_code_uri: https://github.com/SELISEdigitalplatforms/l3-ruby-gem-gotenberg
61
+ changelog_uri: https://github.com/SELISEdigitalplatforms/l3-ruby-gem-gotenberg/blob/main/CHANGELOG.md
62
+ rubygems_mfa_required: 'false'
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 3.0.0
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.5.11
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: A simple Ruby client for gotenberg
82
+ test_files: []