diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..fb55587 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +* @bugloper +.github/ @bugloper @SELISEdigitalplatforms/DEVOPS-ADMIN-ALL-REPOS +CODEOWNERS @bugloper @SELISEdigitalplatforms/DEVOPS-ADMIN-ALL-REPOS diff --git a/.github/ISSUE_TEMPLATE/security_bug_report.md b/.github/ISSUE_TEMPLATE/security_bug_report.md new file mode 100644 index 0000000..4615098 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security_bug_report.md @@ -0,0 +1,55 @@ +--- +name: "Security Bug Report" +about: Report a security vulnerability in the gotenberg gem +title: "[SECURITY] Title of the security issue" +labels: security +assignees: '' + +--- + +## Description + +Please provide a clear and detailed description of the security vulnerability. + +## Steps to Reproduce + +1. List the steps to reproduce the vulnerability. +2. Include any relevant code snippets, configuration files, or logs. +3. Specify any special setup required to reproduce the issue. + +## Expected Behavior + +Describe what you expected to happen. + +## Actual Behavior + +Describe what actually happened. + +## Impact + +Explain the potential impact of this vulnerability. Include details on who might be affected and how severe the impact could be. + +## Versions Affected + +List all versions of the gotenberg gem that are affected by this issue. Include the version of Ruby and any other relevant dependencies. + +## Environment + +- **gotenberg version**: +- **Ruby version**: +- **Operating System**: +- **Other relevant dependencies**: + +## Additional Information + +Provide any additional information that may help us understand the issue and how to fix it. Include links to related issues, stack traces, or other relevant resources. + +## Disclosure + +- [ ] I confirm that I have not disclosed this vulnerability publicly and will not do so until a fix has been released. +- [ ] I agree to follow the gotenberg disclosure policy. + +## Contact Information + +(Optional) If you would like to be contacted regarding this issue, please provide your preferred contact method. + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..02ee861 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "bundler" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..248572f --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,36 @@ +name: Release +on: + push: + branches: + - "main" # main only +jobs: + build: + runs-on: ubuntu-latest + permissions: write-all + steps: + - uses: googleapis/release-please-action@v4 + id: release + # Checkout code if release was created + - uses: actions/checkout@v4 + if: ${{ steps.release.outputs.release_created }} + # Setup ruby if a release was created + - uses: ruby/setup-ruby@v1 + with: + # Not needed with a .ruby-version file + ruby-version: 3.2.2 + # runs 'bundle install' and caches installed gems automatically + bundler-cache: true + if: ${{ steps.release.outputs.release_created }} + + - name: Publish to GPR + if: ${{ steps.release.outputs.release_created }} + run: | + mkdir -p $HOME/.gem + touch $HOME/.gem/credentials + chmod 0600 $HOME/.gem/credentials + printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials + gem build *.gemspec + gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem + env: + GEM_HOST_API_KEY: "Bearer ${{secrets.NEW_GITHUB_TOKEN}}" + OWNER: ${{ github.repository_owner }} diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml new file mode 100644 index 0000000..4c6852a --- /dev/null +++ b/.github/workflows/ruby.yml @@ -0,0 +1,32 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake +# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby + +name: Ruby Setup + +on: + push: + branches: ["main"] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ["3.0", "3.1", "3.2"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby 3 + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + - name: Run tests + run: bundle exec rspec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7185d31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +Gemfile.lock +# rspec failure tracking +.rspec_status diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..fea3454 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.0.0" +} \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..34c5164 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..f174615 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,50 @@ +# AllCops +AllCops: + NewCops: enable + SuggestExtensions: false + TargetRubyVersion: 3.0 + +# Style/FrozenStringLiteralComment +Style/FrozenStringLiteralComment: + Enabled: true + EnforcedStyle: always + +# Layout/LineLength +Layout/LineLength: + Max: 120 + +# Naming/FileName +Naming/FileName: + Enabled: true + Exclude: + - 'spec/**/*' + - 'test/**/*' + +# Layout/IndentationWidth +Layout/IndentationWidth: + Width: 2 + +# Style/StringLiterals +Style/StringLiterals: + EnforcedStyle: double_quotes + +# Lint/UselessAssignment +Lint/UselessAssignment: + Enabled: true + +# Metrics/AbcSize +Metrics/AbcSize: + Max: 100 + +# Metrics/BlockLength +Metrics/BlockLength: + Exclude: + - 'spec/**/*.rb' + +# Metrics/MethodLength +Metrics/MethodLength: + Max: 100 + +# Naming/AccessorMethodName +Naming/AccessorMethodName: + Enabled: false diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..01b2bb9 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,9 @@ +gotenberg maintained by SELISEdigitalplatforms Bhutan. + +### Current Team + +### Alumni + +### Thanks + +We thank the above contributors for their efforts! ๐Ÿ™ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e8d105f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +## [Released] \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..67fe8ce --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md new file mode 100644 index 0000000..8a0c87b --- /dev/null +++ b/CONTRIBUTION.md @@ -0,0 +1,28 @@ +[![Version](https://img.shields.io/gem/v/gotenberg)](https://rubygems.org/gems/gotenberg) +[![License](https://img.shields.io/github/license/bugloper/gotenberg)](https://github.com/SELISEdigitalplatforms/l3-ruby-gem-gotenberg) + +## How to contribute to gotenberg + +#### **Did you find a bug?** + +* [Open a Github issue](https://github.com/SELISEdigitalplatforms/l3-ruby-gem-gotenberg/issues/new) + +* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/SELISEdigitalplatforms/l3-ruby-gem-gotenberg/issues). + + +#### **Did you write a patch that fixes a bug?** + +* Open a new GitHub pull request with the patch. + +* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. + +#### **Do you intend to add a new feature or change an existing one?** + +* Open a Github issue with detailed description + + +gotenberg is a simple gotenberg ruby client and currently it has minimal features and would appreciate your contribution. + +Thanks! :heart: :heart: :heart: + +SELISEdigitalplatforms \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..c719b14 --- /dev/null +++ b/Gemfile @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec + +gem "rake", "~> 13.0" + +group :test do + gem "pry" + gem "rspec", "~> 3.0" + gem "rubocop" + gem "webmock" +end diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9e4e6ad --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) SELISEdigitalplatforms + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9537ff0 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# gotenberg + +`gotenberg` is a simple Ruby client for gotenberg + +## Installation + +Install the gem and add to the application's Gemfile by executing: + + $ bundle add gotenberg + +If bundler is not being used to manage dependencies, install the gem by executing: + + $ gem install gotenberg + +## Usage +```ruby + client = Gotenberg::Client.new(ENV.fetch('GOTENBERG_ENDPOINT', nil)) + pdf_content = client.html(pdf_html) +``` + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/SELISEdigitalplatforms/l3-ruby-gem-gotenberg.git diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..b71d9ad --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,22 @@ +# Releasing gotenberg ๐Ÿฅ‚ + +This document explains releasing process for all gotenberg gems. + +### Releasing + +For releasing new version of `gotenberg`, this is the procedure: + +1. Update `CHANGELOG.md` +2. Update `VERSION` file with target version +3. Run `rake release:commit_version` +4. Create pull request with all that ([example](https://github.com/SELISEdigitalplatforms/l3-ruby-gem-gotenberg/pull/69)) +5. Merge the pull request when CI is green +6. Ensure you have latest changes locally +7. Run `rake release:tag_version` +8. Push tag to upstream +9. Run `rake release:watch` and watch GitHub Actions push to RubyGems.org + +### Packaging + +### Packaging and installing locally + \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..b6ae734 --- /dev/null +++ b/Rakefile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +task default: :spec diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..78bae9e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,43 @@ +# Reporting a security bug + + +All security bugs in gotenberg should be reported to the core team through our private mailing list [selise@selisegroup.com]. Your report will be acknowledged within 24 hours, and youโ€™ll receive a more detailed response to your email within 48 hours indicating the next steps in handling your report. + +After the initial reply to your report, the security team will endeavor to keep you informed of the progress being made towards a fix and full announcement. These updates will be sent at least every five days, although in practice, it is more likely to be every 24-48 hours. + +If you have not received a reply to your email within 48 hours, or have not heard from the security team for the past five days, there are a few steps you can take: + +## Disclosure Policy + +gotenberg follows a 5-step disclosure policy, which is upheld to the best of our ability. + +1. Security report received and is assigned a primary handler. This person will coordinate the fix and release process. +2. Problem is confirmed and a list of all affected versions is determined. Code is audited to find any potential similar problems. +3. Fixes are prepared for all releases which are still supported. These fixes are not committed to the public repository but rather held locally pending the announcement. +4. A suggested embargo date for this vulnerability is chosen and distros@openwall is notified. This notification will include patches for all versions still under support and a contact address for packagers who need advice back-porting patches to older versions. +5. On the embargo date, the [mailing list][mailing-list] and [security list][security-list] are sent a copy of the announcement. The changes are pushed to the public repository and new gems released to RubyGems. + +Typically, the embargo date will be set 72 hours from the time vendor-sec is first notified, however, this may vary depending on the severity of the bug or difficulty in applying a fix. + +This process can take some time, especially when coordination is required with maintainers of other projects. Every effort will be made to handle the bug in as timely a manner as possible, however, itโ€™s important that we follow the release process above to ensure that the disclosure is handled in a consistent manner. + +## Security Updates + +Security updates will be posted on the [mailing list][mailing-list] and [security list][security-list]. + +## Comments on this Policy + +If you have any suggestions to improve this policy, please email the core team at [selise@selisegroup.com]. + +## Credit for Reporters + +We highly appreciate the efforts of security researchers who report vulnerabilities to us. We offer the following forms of recognition: + +TODO: Decide on these +* Reporters will be listed on the project's official documentation site. +* Reporters will be acknowledged in the security advisories and release notes associated with the fix. + +We strive to ensure that reporters receive due credit for their valuable contributions to the security of gotenberg. + +[mailing-list]: http://groups.google.com/group/gotenberg/topics +[security-list]: http://groups.google.com/group/gotenberg-security/topics diff --git a/gotenberg.gemspec b/gotenberg.gemspec new file mode 100644 index 0000000..a5da1b9 --- /dev/null +++ b/gotenberg.gemspec @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative "lib/gotenberg/version" + +Gem::Specification.new do |spec| + spec.name = "gotenberg" + spec.version = Gotenberg::VERSION + spec.licenses = ["MIT"] + spec.authors = %w[bugloper teknatha136] + spec.email = ["bugloper@gmail.com"] + + spec.summary = "A simple Ruby client for gotenberg" + spec.description = "A simple Ruby client for gotenberg" + spec.homepage = "https://github.com/SELISEdigitalplatforms/l3-ruby-gem-gotenberg" + spec.required_ruby_version = ">= 3.0.0" + + spec.metadata["allowed_push_host"] = "https://rubygems.pkg.github.com/SELISEdigitalplatforms" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://github.com/SELISEdigitalplatforms/l3-ruby-gem-gotenberg" + spec.metadata["changelog_uri"] = "https://github.com/SELISEdigitalplatforms/l3-ruby-gem-gotenberg/blob/main/CHANGELOG.md" + File.basename(__FILE__) + spec.files = Dir["lib/**/*.rb"] + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + spec.add_dependency "mime-types" + spec.add_dependency "multipart-post", "~> 2.1" + # rubocop:disable Gemspec/RequireMFA + spec.metadata["rubygems_mfa_required"] = "false" + # rubocop:enable Gemspec/RequireMFA +end diff --git a/lib/gotenberg.rb b/lib/gotenberg.rb new file mode 100644 index 0000000..d072c90 --- /dev/null +++ b/lib/gotenberg.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative "gotenberg/version" +require_relative "gotenberg/client" +require_relative "gotenberg/railtie" if defined?(Rails::Railtie) +require_relative "gotenberg/helper" + +module Gotenberg + class GotenbergDownError < StandardError; end +end diff --git a/lib/gotenberg/client.rb b/lib/gotenberg/client.rb new file mode 100644 index 0000000..bccc9b2 --- /dev/null +++ b/lib/gotenberg/client.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require "net/http" +require "uri" +require "json" +require "mime/types" +require "net/http/post/multipart" +require "tempfile" +require "securerandom" +require "fileutils" + +module Gotenberg + class IndexFileMissing < StandardError; end + + # Client class for interacting with the Gotenberg API + class Client + # Initialize a new Client + # + # @param api_url [String] The base URL of the Gotenberg API + def initialize(api_url) + @api_url = api_url + end + + # Convert HTML files to PDF and write it to the output file + # + # @param htmls [Hash{Symbol => String}] A hash with the file name as the key and the HTML content as the value + # @param asset_paths [Array] Paths to the asset files (like CSS, images) required by the HTML files + # @param properties [Hash] Additional properties for PDF conversion + # @option properties [Float] :paperWidth The width of the paper + # @option properties [Float] :paperHeight The height of the paper + # @option properties [Float] :marginTop The top margin + # @option properties [Float] :marginBottom The bottom margin + # @option properties [Float] :marginLeft The left margin + # @option properties [Float] :marginRight The right margin + # @option properties [Boolean] :preferCssPageSize Whether to prefer CSS page size + # @option properties [Boolean] :printBackground Whether to print the background + # @option properties [Boolean] :omitBackground Whether to omit the background + # @option properties [Boolean] :landscape Whether to use landscape orientation + # @option properties [Float] :scale The scale of the PDF + # @option properties [String] :nativePageRanges The page ranges to include + # @return [String] The resulting PDF content + # @raise [GotenbergDownError] if the Gotenberg API is down + # + # Example: + # htmls = { index: "

Html

", header: "

Header

", footer: "

Footer

" } + # asset_paths = ["path/to/style.css", "path/to/image.png"] + # properties = { paperWidth: 8.27, paperHeight: 11.7, marginTop: 1, marginBottom: 1 } + # client = Gotenberg::Client.new("http://localhost:3000") + # pdf_content = client.html(htmls, asset_paths, properties) + # + def html(htmls, asset_paths, properties = {}) # rubocop:disable Metrics/CyclomaticComplexity + raise GotenbergDownError unless up? + + raise IndexFileMissing unless (htmls.keys & ["index", :index]).any? + + dir_name = SecureRandom.uuid + dir_path = File.join(Dir.tmpdir, dir_name) + FileUtils.mkdir_p(dir_path) + + htmls.each do |key, value| + File.write(File.join(dir_path, "#{key}.html"), value) + end + + uri = URI("#{@api_url}/forms/chromium/convert/html") + + # Gotenberg requires all files to be in the same directory + asset_paths.each do |path| + FileUtils.cp(path, dir_path) + end + + # Rejecting .. and . + entries = Dir.entries(dir_path).reject { |f| f.start_with?(".") } + + payload = entries.each_with_object({}).with_index do |(entry, obj), index| + entry_abs_path = File.join(dir_path, entry) + mime_type = MIME::Types.type_for(entry_abs_path).first.content_type + obj["files[#{index}]"] = UploadIO.new(entry_abs_path, mime_type) + end + + response = multipart_post(uri, payload.merge(properties)) + response.body.dup.force_encoding("utf-8") + ensure + FileUtils.rm_rf(dir_path) if dir_path + end + + # Check if the Gotenberg API is up and healthy + # + # @return [Boolean] true if the API is up, false otherwise + def up? + uri = URI("#{@api_url}/health") + request = Net::HTTP::Get.new(uri) + request.basic_auth( + ENV.fetch("GOTENBERG_API_BASIC_AUTH_USERNAME", nil), + ENV.fetch("GOTENBERG_API_BASIC_AUTH_PASSWORD", nil) + ) + + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = uri.scheme == "https" + response = http.request(request) + + response.is_a?(Net::HTTPSuccess) && JSON.parse(response.body)["status"] == "up" + rescue StandardError + false + end + + private + + def multipart_post(uri, payload) + request = Net::HTTP::Post::Multipart.new(uri.path, payload) + request.basic_auth( + ENV.fetch("GOTENBERG_API_BASIC_AUTH_USERNAME", nil), + ENV.fetch("GOTENBERG_API_BASIC_AUTH_PASSWORD", nil) + ) + + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = uri.scheme == "https" + http.request(request) + end + end + + # Custom error class for Gotenberg API downtime + class GotenbergDownError < StandardError; end +end diff --git a/lib/gotenberg/helper.rb b/lib/gotenberg/helper.rb new file mode 100644 index 0000000..977108d --- /dev/null +++ b/lib/gotenberg/helper.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +module Gotenberg + module Helper # rubocop:disable Style/Documentation + class ExtensionMissing < StandardError; end + + class PropshaftAsset # rubocop:disable Style/Documentation + attr_reader :asset + + def initialize(asset) + @asset = asset + end + + def content_type + asset.content_type.to_s + end + + def to_s + asset.content + end + + def filename + asset.path.to_s + end + end + + class MissingAsset < StandardError # rubocop:disable Style/Documentation + attr_reader :path + + def initialize(path, message) + @path = path + super(message) + end + end + + class LocalAsset # rubocop:disable Style/Documentation + attr_reader :path + + def initialize(path) + @path = path + end + + def content_type + Mime::Type.lookup_by_extension(File.extname(path).delete(".")) + end + + def to_s + File.read(path) + end + + def filename + path.to_s + end + end + + class SprocketsEnvironment # rubocop:disable Style/Documentation + def self.instance + @instance ||= Sprockets::Railtie.build_environment(Rails.application) + end + + def self.find_asset(*args) + instance.find_asset(*args) + end + end + + def goten_asset_base64(asset_name) + asset = find_asset(goten_static_asset_path(asset_name)) + raise MissingAsset.new(asset_name, "Could not find asset '#{asset_name}'") if asset.nil? + + base64 = Base64.encode64(asset.to_s).delete("\n") + "data:#{asset.content_type};base64,#{Rack::Utils.escape(base64)}" + end + + def goten_static_asset_path(asset_name) + ext = File.extname(asset_name).delete(".") + + raise ExtensionMissing if ext.empty? + + asset_type = + case ext + when "js" then "javascripts" + when "css" then "stylesheets" + else "images" + end + + determine_static_path(asset_type, asset_name) + end + + def goten_compiled_asset_path(asset_name) + Rails.public_path.to_s + + ActionController::Base.helpers.asset_path(asset_name) + end + + private + + def determine_static_path(asset_type, asset_name) + asset_root = Rails.root.join("app", "assets") + path = asset_root.join(asset_type, asset_name) + + unless File.exist?(path) + raise MissingAsset.new( + asset_name, + "Could not find static asset '#{asset_name}'" + ) + end + + path.to_s + end + + # Thanks WickedPDF ๐Ÿ™ + def find_asset(path) + if Rails.application.assets.respond_to?(:find_asset) + Rails.application.assets.find_asset(path, base_path: Rails.application.root.to_s) + elsif defined?(Propshaft::Assembly) && Rails.application.assets.is_a?(Propshaft::Assembly) + PropshaftAsset.new(Rails.application.assets.load_path.find(path)) + elsif Rails.application.respond_to?(:assets_manifest) + asset_path = File.join(Rails.application.assets_manifest.dir, Rails.application.assets_manifest.assets[path]) + LocalAsset.new(asset_path) if File.file?(asset_path) + else + SprocketsEnvironment.find_asset(path, base_path: Rails.application.root.to_s) + end + end + end +end diff --git a/lib/gotenberg/railtie.rb b/lib/gotenberg/railtie.rb new file mode 100644 index 0000000..7a9e2da --- /dev/null +++ b/lib/gotenberg/railtie.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "rails" +require "gotenberg/helper" + +module Gotenberg + class Railtie < Rails::Railtie # rubocop:disable Style/Documentation + initializer "gotenberg.register" do + ActiveSupport.on_load :action_view do + include Gotenberg::Helper + end + end + end +end diff --git a/lib/gotenberg/version.rb b/lib/gotenberg/version.rb new file mode 100644 index 0000000..b3e2647 --- /dev/null +++ b/lib/gotenberg/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Gotenberg + VERSION = "0.0.0" +end diff --git a/pull_request_template.md b/pull_request_template.md new file mode 100644 index 0000000..2468196 --- /dev/null +++ b/pull_request_template.md @@ -0,0 +1,26 @@ +## Describe your changes +Please include a detailed summary of the changes and the related issue. Also include any relevant context. List any dependencies that are required for this change. + +## Related issue number +Mention the related issue number (e.g., "Fixes #123"). + +## Checklist before requesting a review +- [ ] I have performed a self-review of my code +- [ ] Ran code linter and addressed any issues. +- [ ] My code follows the style guidelines of this project +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes + +## Type of change +_You may remove the options that are no relevant._ + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update +- [ ] Code style update (formatting, local variables) +- [ ] Refactoring (no functional changes, no API changes) +- [ ] Build & CI Related Changes +- [ ] Others (Please describe) diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..41a5a70 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "draft": false, + "include-component-in-tag": true, + "include-v-in-tag": true, + "prerelease": false, + "skip-github-release": false, + "separate-pull-requests": true, + "tag-separator": "/", + "sequential-calls": true, + "packages": { + ".": { + "release-type": "ruby", + "package-name": "gotenberg" + } + } +} \ No newline at end of file diff --git a/spec/client_spec.rb b/spec/client_spec.rb new file mode 100644 index 0000000..2d27ccd --- /dev/null +++ b/spec/client_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "rspec" +require "webmock/rspec" +require "json" +require_relative "../lib/gotenberg/client" + +RSpec.describe Gotenberg::Client do + let(:api_url) { "http://localhost:3000" } + let(:client) { described_class.new(api_url) } + let(:html_content) { { index: "

Hello, world!

" } } + let(:asset_paths) { [] } + + describe "#html" do + context "when the Gotenberg API is up" do + before do + stub_request(:get, "#{api_url}/health").to_return( + body: { status: "up" }.to_json, + headers: { "Content-Type" => "application/json" }, + status: 200 + ) + end + + it "converts HTML to PDF" do + stub_request(:post, "#{api_url}/forms/chromium/convert/html") + .to_return(body: "PDF content", status: 200) + + pdf_content = client.html(html_content, asset_paths) + + expect(pdf_content).to eq("PDF content") + end + end + + context "when the Gotenberg API is down" do + before do + stub_request(:get, "#{api_url}/health").to_return( + body: { status: "down" }.to_json, + headers: { "Content-Type" => "application/json" }, + status: 500 + ) + end + + it "raises a GotenbergDownError" do + expect { client.html(html_content, asset_paths) }.to raise_error(Gotenberg::GotenbergDownError) + end + end + end + + describe "#up?" do + context "when the Gotenberg API is up" do + it "returns true" do + stub_request(:get, "#{api_url}/health").to_return( + body: { status: "up" }.to_json, + headers: { "Content-Type" => "application/json" }, + status: 200 + ) + + expect(client.up?).to eq(true) + end + end + + context "when the Gotenberg API is down" do + it "returns false" do + stub_request(:get, "#{api_url}/health").to_return( + body: { status: "down" }.to_json, + headers: { "Content-Type" => "application/json" }, + status: 500 + ) + + expect(client.up?).to eq(false) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..3269e19 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "gotenberg" +require "webmock/rspec" + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end diff --git a/spec/version_spec.rb b/spec/version_spec.rb new file mode 100644 index 0000000..41175cd --- /dev/null +++ b/spec/version_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.describe Gotenberg do + it "has a version number" do + expect(Gotenberg::VERSION).not_to be nil + end +end