gotenberg 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []