pdf_mage 0.1.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: b94ce6a010e8a551ca483651eb2998f2341d4f70d937f95de3737bf5eb063388
4
+ data.tar.gz: 11524a2c1e1e2a43b673616301510eab6a47c2919ac578ff653683d57b2552c1
5
+ SHA512:
6
+ metadata.gz: 44f11f5a1d38e3eb1d221fee75349d618ff054c13554e667470987bd87a54c3a280456794e75f8d2b332af6a9a8677a8d6c75ab0b9cac57e51dfea9169f1767e
7
+ data.tar.gz: 0b107577a60a86e1353905b29c71d0cd7341d1f7c8a3141a0262e04e9bf53f9536fa5dd28dc5631dd8ff78a850a071260dc22f5eb7ca788ffe60424fa7749af7
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ repo_token: 5DNjehW2z6VK8jjiE5qMyGlUqN4t8UBHM
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .DS_Store
10
+ /pdfs/
11
+ *.local
12
+ *.local.yml
13
+ *.log
14
+ /spec/examples.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,23 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.5.1
3
+
4
+ Metrics/AbcSize:
5
+ Max: 25
6
+
7
+ Metrics/BlockLength:
8
+ Exclude:
9
+ - spec/**/*
10
+
11
+ Metrics/LineLength:
12
+ Max: 120
13
+
14
+ Metrics/MethodLength:
15
+ Max: 25
16
+ Exclude:
17
+ - spec/**/*
18
+
19
+ Metrics/CyclomaticComplexity:
20
+ Max: 8
21
+
22
+ Metrics/PerceivedComplexity:
23
+ Max: 8
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ cache: bundler
3
+ addons:
4
+ chrome: stable
5
+ script:
6
+ - bundle exec rubocop
7
+ - bundle exec rspec
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at dean.g.papastrat@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,113 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pdf_mage (0.1.0)
5
+ aws-sdk-s3 (~> 1)
6
+ redis (~> 4.0)
7
+ sidekiq (~> 5.1)
8
+ sinatra (~> 2.0)
9
+ typhoeus (~> 1.1)
10
+
11
+ GEM
12
+ remote: https://rubygems.org/
13
+ specs:
14
+ ast (2.4.0)
15
+ aws-eventstream (1.0.0)
16
+ aws-partitions (1.90.0)
17
+ aws-sdk-core (3.21.2)
18
+ aws-eventstream (~> 1.0)
19
+ aws-partitions (~> 1.0)
20
+ aws-sigv4 (~> 1.0)
21
+ jmespath (~> 1.0)
22
+ aws-sdk-kms (1.5.0)
23
+ aws-sdk-core (~> 3)
24
+ aws-sigv4 (~> 1.0)
25
+ aws-sdk-s3 (1.13.0)
26
+ aws-sdk-core (~> 3, >= 3.21.2)
27
+ aws-sdk-kms (~> 1)
28
+ aws-sigv4 (~> 1.0)
29
+ aws-sigv4 (1.0.2)
30
+ concurrent-ruby (1.0.5)
31
+ connection_pool (2.2.2)
32
+ coveralls (0.8.21)
33
+ json (>= 1.8, < 3)
34
+ simplecov (~> 0.14.1)
35
+ term-ansicolor (~> 1.3)
36
+ thor (~> 0.19.4)
37
+ tins (~> 1.6)
38
+ diff-lcs (1.3)
39
+ docile (1.1.5)
40
+ ethon (0.11.0)
41
+ ffi (>= 1.3.0)
42
+ ffi (1.9.24)
43
+ jmespath (1.4.0)
44
+ json (2.1.0)
45
+ mustermann (1.0.2)
46
+ parallel (1.12.1)
47
+ parser (2.5.1.0)
48
+ ast (~> 2.4.0)
49
+ powerpack (0.1.1)
50
+ rack (2.0.5)
51
+ rack-protection (2.0.1)
52
+ rack
53
+ rainbow (3.0.0)
54
+ rake (10.5.0)
55
+ redis (4.0.1)
56
+ rspec (3.7.0)
57
+ rspec-core (~> 3.7.0)
58
+ rspec-expectations (~> 3.7.0)
59
+ rspec-mocks (~> 3.7.0)
60
+ rspec-core (3.7.1)
61
+ rspec-support (~> 3.7.0)
62
+ rspec-expectations (3.7.0)
63
+ diff-lcs (>= 1.2.0, < 2.0)
64
+ rspec-support (~> 3.7.0)
65
+ rspec-mocks (3.7.0)
66
+ diff-lcs (>= 1.2.0, < 2.0)
67
+ rspec-support (~> 3.7.0)
68
+ rspec-support (3.7.1)
69
+ rubocop (0.56.0)
70
+ parallel (~> 1.10)
71
+ parser (>= 2.5)
72
+ powerpack (~> 0.1)
73
+ rainbow (>= 2.2.2, < 4.0)
74
+ ruby-progressbar (~> 1.7)
75
+ unicode-display_width (~> 1.0, >= 1.0.1)
76
+ ruby-progressbar (1.9.0)
77
+ sidekiq (5.1.3)
78
+ concurrent-ruby (~> 1.0)
79
+ connection_pool (~> 2.2, >= 2.2.0)
80
+ rack-protection (>= 1.5.0)
81
+ redis (>= 3.3.5, < 5)
82
+ simplecov (0.14.1)
83
+ docile (~> 1.1.0)
84
+ json (>= 1.8, < 3)
85
+ simplecov-html (~> 0.10.0)
86
+ simplecov-html (0.10.2)
87
+ sinatra (2.0.1)
88
+ mustermann (~> 1.0)
89
+ rack (~> 2.0)
90
+ rack-protection (= 2.0.1)
91
+ tilt (~> 2.0)
92
+ term-ansicolor (1.6.0)
93
+ tins (~> 1.0)
94
+ thor (0.19.4)
95
+ tilt (2.0.8)
96
+ tins (1.16.3)
97
+ typhoeus (1.3.0)
98
+ ethon (>= 0.9.0)
99
+ unicode-display_width (1.3.3)
100
+
101
+ PLATFORMS
102
+ ruby
103
+
104
+ DEPENDENCIES
105
+ bundler (~> 1.16)
106
+ coveralls (~> 0.8)
107
+ pdf_mage!
108
+ rake (~> 10.0)
109
+ rspec (~> 3.7)
110
+ rubocop (~> 0.56)
111
+
112
+ BUNDLED WITH
113
+ 1.16.2
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Sideqik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # PdfMage ![Build Status](https://travis-ci.org/sideqik/pdf-mage.svg?branch=master) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/49a43b30df054740910ac010042372bb)](https://www.codacy.com/app/Sideqik/pdf-mage?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=sideqik/pdf-mage&amp;utm_campaign=Badge_Grade) [![Coverage Status](https://coveralls.io/repos/github/sideqik/pdf-mage/badge.svg?branch=master)](https://coveralls.io/github/sideqik/pdf-mage?branch=master)
2
+
3
+ PdfMage is a standalone microservice packaged as a Ruby gem that you makes it simple to render PDFs using Headless Chrome on a standalone server.
4
+
5
+ It includes support for:
6
+
7
+ - Uploading PDFs to AWS S3
8
+ - Webhooks
9
+ - API "secrets" for security
10
+
11
+ Please note, the documentation is still in-progress!
12
+
13
+ ## Installation
14
+
15
+ Clone the repository to your computer and navigate to the directory:
16
+
17
+ ```sh
18
+ git clone https://github.com/sideqik/pdf-mage.git
19
+ cd pdf-mage
20
+ ```
21
+
22
+ Then install bundler if you haven't already done so:
23
+
24
+ ```sh
25
+ gem install bundler
26
+ ```
27
+
28
+ Finally, run the setup command:
29
+
30
+ ```sh
31
+ bin/setup
32
+ ```
33
+
34
+ ## Running the Server
35
+
36
+ To run the server, you need to turn run both the API and the Sidekiq job server. If running in production, make sure to enable the environment flag for it, or else it'll default to the development environment.
37
+
38
+ ```sh
39
+ PDFMAGE_ENV=production bin/pdf_mage
40
+ PDFMAGE_ENV=production bin/sidekiq
41
+ ```
42
+
43
+ We recommend using a service such as `monit` to keep these processes alive in production, as system issues could cause them to crash and not be restarted.
44
+
45
+ ## Configuration
46
+
47
+ Create a config.yml.local file and overwrite any of the following properties:
48
+
49
+ | name | type | description |
50
+ | ---- | ---- | ----------- |
51
+ | api_secret | String | TBD |
52
+ | aws_account_key | String | Key in your AWS S3 API credentials
53
+ | aws_account_bucket | String | Bucket to upload PDFs into
54
+ | aws_account_region | String | Region to upload PDFs into
55
+ | aws_account_secret | String | Secret in your AWS S3 API credentials
56
+ | aws_presigned_url_duration | Integer | How long the returned URL in the presigned-request should exist for, in seconds
57
+ | chrome_exe | String | Path to the Chrome executable
58
+ | delete_file_on_upload | Boolean | Whether or not to delete PDFs after they're uploaded
59
+ | log_level | String | What level of logs to write to the logfile
60
+ | pdf_directory | String | Where to locally store PDFs
61
+
62
+ Then, when starting the server, pass `CONFIG_FILE=config.yml.local`.
63
+
64
+ ```sh
65
+ CONFIG_FILE=config.yml.local bin/pdf_mage
66
+ CONFIG_FILE=config.yml.local bin/sidekiq
67
+ ```
68
+
69
+ ### Using the "Secret" for Security
70
+
71
+ If you use the API secret config option, you can add security to your application. Adding a secret will:
72
+
73
+ 1. Require you to pass a "secret" param with every request to PDF Mage's render resource, where the value is the secret set in the config.
74
+ 2. Request the website url you want PDF Mage to render with a "secret" parameter, with the value you set in the config.
75
+ 3. Sign the webhook sent to your server with a SHA256 hexdigest as a "X-Pdf-Signature" header. See info about checking the signature below.
76
+
77
+ **To check the X-Pdf-Signature header:**
78
+
79
+ 1. Create a SHA256 HMAC hexdigest using the config secret as the key and the response body as the data.
80
+ 2. Compare the hexdigest to the signature provided.
81
+
82
+ **Example in Ruby:**
83
+
84
+ ```ruby
85
+ # Generate the signature that should have been returned
86
+ valid_signature = OpenSSL::HMAC.hexdigest('sha256', CONFIG.api_secret, response.body)
87
+
88
+ # Compare the signatures
89
+ response.headers['X-Pdf-Signature'] == valid_signature
90
+ ```
91
+
92
+ ## Development
93
+
94
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
95
+
96
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
97
+
98
+ ## Contributing
99
+
100
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sidekiq/pdf_mage. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
101
+
102
+ ## License
103
+
104
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
105
+
106
+ ## Code of Conduct
107
+
108
+ Everyone interacting in the PdfMage project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/sidekiq/pdf_mage/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'pdf_mage'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require 'pry'
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/pdf_mage ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require 'pdf_mage'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ path = File.expand_path('../lib', __dir__)
9
+ $LOAD_PATH.unshift(path) if File.directory?(path) && !$LOAD_PATH.include?(path)
10
+ require 'pdf_mage'
11
+ end
12
+
13
+ PdfMage.start!
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/bin/sidekiq ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bash
2
+ bundle exec sidekiq -r ./lib/pdf_mage_workers.rb -q pdfmage
data/lib/pdf_mage.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ MODE = 'api'
4
+
5
+ require 'pdf_mage/init'
6
+ require 'pdf_mage/api/app'
7
+
8
+ # Root loader for all PdfMage classes & modules
9
+ module PdfMage
10
+ # Starts the PdfMage API Application
11
+ # @return [NilClass]
12
+ def self.start!
13
+ PdfMage::Api::App.run!
14
+ nil
15
+ end
16
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra'
4
+ require 'json'
5
+ require 'pdf_mage/workers/render_pdf'
6
+
7
+ module PdfMage
8
+ module Api
9
+ # The PdfMage Sinatra API application, which has /status and /render endpoints that you can call to render PDFs
10
+ # from another server over HTTPS.
11
+ # @since 0.1.0
12
+ class App < Sinatra::Base
13
+ set :environment, :production
14
+
15
+ before do
16
+ content_type :json
17
+ end
18
+
19
+ error do
20
+ e = env['sinatra.error']
21
+ error(500, e.message)
22
+ end
23
+
24
+ get '/status' do
25
+ { result: 'ok' }.to_json
26
+ end
27
+
28
+ post '/render' do
29
+ authorize!
30
+ url = required_param(:url)
31
+ callback_url = params[:callback_url]
32
+ filename = params[:filename]
33
+ meta = params[:meta]
34
+
35
+ job_id = PdfMage::Workers::RenderPdf.perform_async(url, callback_url, filename, meta)
36
+ { result: 'ok', job_id: job_id }.to_json
37
+ end
38
+
39
+ def authorize!
40
+ return unless CONFIG.api_secret
41
+ error(401, 'Unauthorized') unless params[:secret] && params[:secret].strip == CONFIG.api_secret
42
+ end
43
+
44
+ def error(code, message)
45
+ halt code, { result: 'error', message: message }.to_json
46
+ end
47
+
48
+ def required_param(key)
49
+ value = params[key]
50
+ error(422, "Required parameter '#{key}' is missing") unless value && !value.empty?
51
+ value
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,10 @@
1
+ api_secret: null
2
+ aws_account_key: null
3
+ aws_account_bucket: null
4
+ aws_account_region: 'us-east-1'
5
+ aws_account_secret: null
6
+ aws_presigned_url_duration: 600
7
+ chrome_exe: chrome
8
+ delete_file_on_upload: true
9
+ log_level: 'info'
10
+ pdf_directory: 'pdfs'
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'ostruct'
5
+ require 'yaml'
6
+
7
+ ENVIRONMENTS = %w[development test staging production].freeze
8
+
9
+ # Load Environment
10
+ current_env = ENV['PDFMAGE_ENV'] || 'development'
11
+ env_config = OpenStruct.new
12
+ env_config.current = current_env
13
+ ENVIRONMENTS.each { |env| env_config[env] = current_env == env }
14
+
15
+ # Load Config Files
16
+ user_config_filename = ENV['CONFIG_FILE'] ? File.expand_path(ENV['CONFIG_FILE']) : 'config.local.yml'
17
+ default_config_file = File.new(File.expand_path('lib/pdf_mage/config.yml'))
18
+
19
+ # Build Configuration
20
+ config_hash = YAML.safe_load(default_config_file).to_h
21
+
22
+ if File.exist?(user_config_filename)
23
+ user_config_file = File.new(user_config_filename)
24
+ user_config = YAML.safe_load(user_config_file).to_h
25
+ config_hash = config_hash.merge(user_config)
26
+ end
27
+
28
+ config = OpenStruct.new(config_hash)
29
+ config.env = env_config.freeze
30
+ CONFIG = config.freeze
31
+
32
+ # Build Logger
33
+ LOGGER = Logger.new(STDOUT)
34
+ LOGGER.level = CONFIG.log_level
35
+ LOGGER.freeze
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PdfMage
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'sidekiq'
5
+ require 'uri'
6
+
7
+ module PdfMage
8
+ module Workers
9
+ # Base worker class that configures Sidekiq options and all workers extend.
10
+ # @since 0.1.0
11
+ class Base
12
+ include Sidekiq::Worker
13
+ sidekiq_options queue: 'pdfmage'
14
+
15
+ # Options for the strip string method, for use with the String#encode method.
16
+ # @private
17
+ STRIP_STRING_OPTIONS = {
18
+ invalid: :replace,
19
+ undef: :replace,
20
+ replace: '',
21
+ universal_newline: true
22
+ }.freeze
23
+
24
+ # Creates directories in the filesystem for the given filename, so that writing a file to that location succeeds.
25
+ #
26
+ # @param [String] filename - string that represents the path the PDF will be created at
27
+ # @return [NilClass]
28
+ #
29
+ # @raise [ArgumentError] if filename is nil or an empty string
30
+ def ensure_directory_exists_for_pdf(filename)
31
+ unless string_exists?(filename)
32
+ raise ArgumentError, 'filename must be a string that includes at least 1 ASCII character.'
33
+ end
34
+
35
+ directory_path = filename.split('/').slice(0..-2).join('/')
36
+ FileUtils.mkdir_p(directory_path) if string_exists?(directory_path)
37
+ nil
38
+ end
39
+
40
+ # Generates a filename for a unique PDF identifier using the pdf directory specified in the config and the given
41
+ # pdf id.
42
+ #
43
+ # @param [String] pdf_id - PDF identifier to make filename from
44
+ # @return [String] filename to store PDF at
45
+ #
46
+ # @raise [ArgumentError] if pdf_id is nil or an empty string
47
+ # @raise [ArgumentError] if CONFIG.pdf_directory is nil or an empty string
48
+ def pdf_filename(pdf_id)
49
+ return @filename if defined?(@filename)
50
+
51
+ unless string_exists?(pdf_id)
52
+ raise ArgumentError, 'pdf_id must be a string that includes at least 1 ASCII character.'
53
+ end
54
+
55
+ unless string_exists?(CONFIG.pdf_directory)
56
+ raise ArgumentError, '
57
+ The pdf_directory in your config.yml must be a string that includes at least 1 ASCII character.
58
+ '
59
+ end
60
+
61
+ filename = "#{CONFIG.pdf_directory}/#{pdf_id}"
62
+ filename += '.pdf' unless pdf_id.end_with?('.pdf')
63
+
64
+ @filename = filename
65
+ end
66
+
67
+ # Adds the API secret to a URL.
68
+ #
69
+ # @param [String] url - URL to add the API secret from the config to
70
+ # @return [String] url with secret
71
+ #
72
+ # @raise [ArgumentError] if url is nil or an empty string
73
+ def secretize_url(url)
74
+ unless string_exists?(url) && (uri = URI(url)) && uri.scheme&.match(/^https?$/)
75
+ raise ArgumentError, 'url must be a valid url using the http/s protocol.'
76
+ end
77
+
78
+ if CONFIG.api_secret
79
+ new_query_params = URI.decode_www_form(uri.query.to_s) << ['secret', CONFIG.api_secret]
80
+ uri.query = URI.encode_www_form(new_query_params)
81
+ uri.to_s
82
+ else
83
+ url
84
+ end
85
+ end
86
+
87
+ # Checks if the given string is not nil and is not empty, like ActiveSupport's String#present?
88
+ #
89
+ # @param [String] string - string to strip non-ASCII characters from
90
+ # @return [TrueClass, FalseClass] boolean of if the string is present or not
91
+ def string_exists?(string)
92
+ !string.nil? && !string.empty?
93
+ end
94
+
95
+ # Removes all non-ASCII characters from a string.
96
+ #
97
+ # @param [String] string - string to strip non-ASCII characters from
98
+ # @return [String, NilClass] string with non-ASCII characters removed or nil if given nil
99
+ def strip_string(string)
100
+ string&.encode(Encoding.find('ASCII'), STRIP_STRING_OPTIONS)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'send_webhook'
5
+ require_relative 'upload_file'
6
+ require 'securerandom'
7
+
8
+ module PdfMage
9
+ module Workers
10
+ # A Sidekiq job that renders a PDF using Chrome headless and kicks off post-render tasks, such as webhook
11
+ # processing or uploading to Amazon S3.
12
+ # @since 0.1.0
13
+ class RenderPdf < PdfMage::Workers::Base
14
+ def perform(website_url, callback_url = nil, filename = nil, meta = nil)
15
+ LOGGER.info "Rendering [#{website_url}] with callback [#{callback_url}] and meta: #{meta.inspect}"
16
+
17
+ stripped_filename = strip_string(filename)
18
+ stripped_filename_present = string_exists?(stripped_filename)
19
+
20
+ # If a filename exists and the stripped version causes the string to be empty, warn about it in the logs.
21
+ if filename && !stripped_filename_present
22
+ LOGGER.warn "'#{filename}' is not a valid ASCII string, falling back to UUID for PDF name."
23
+ end
24
+
25
+ pdf_id = stripped_filename_present ? stripped_filename : SecureRandom.uuid
26
+ ensure_directory_exists_for_pdf(pdf_filename(pdf_id))
27
+ url_with_secret = secretize_url(website_url)
28
+
29
+ `#{CONFIG.chrome_exe} --headless --disable-gpu --print-to-pdf=#{pdf_filename(pdf_id)} #{url_with_secret}`
30
+
31
+ raise "Error executing chrome PDF export. Status: [#{status}]" unless $CHILD_STATUS.zero?
32
+ LOGGER.info "Rendered PDF [#{pdf_id}] with status [#{status}]"
33
+
34
+ if CONFIG.aws_account_key
35
+ PdfMage::Workers::UploadFile.perform_async(pdf_id, callback_url, meta)
36
+ elsif string_exists?(callback_url)
37
+ PdfMage::Workers::SendWebhook.perform_async(pdf_filename(pdf_id), callback_url, meta)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'typhoeus'
5
+
6
+ module PdfMage
7
+ module Workers
8
+ # A Sidekiq job that sends a webhook after a PDF has been rendered or uploaded.
9
+ # @since 0.1.0
10
+ class SendWebhook < PdfMage::Workers::Base
11
+ def perform(file_url_or_path, callback_url, meta = nil)
12
+ LOGGER.info "Sending webhook to [#{callback_url}] for file [#{file_url_or_path}] and meta: #{meta.inspect}"
13
+ params = {
14
+ file: file_url_or_path,
15
+ meta: meta
16
+ }
17
+
18
+ data = params.to_json
19
+ headers = {
20
+ 'content-type' => 'application/json',
21
+ 'user-agent' => 'PdfMage/1.0'
22
+ }
23
+
24
+ headers['X-Pdf-Signature'] = OpenSSL::HMAC.hexdigest('sha256', CONFIG.api_secret, data) if CONFIG.api_secret
25
+
26
+ response = Typhoeus.post(callback_url, headers: headers, body: data, ssl_verifypeer: !CONFIG.env.development)
27
+ LOGGER.info "Received response with status [#{response.code}]: #{response.body}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'send_webhook'
5
+ require 'aws-sdk-s3'
6
+
7
+ module PdfMage
8
+ module Workers
9
+ # A Sidekiq job that uploads a rendered PDF to Amazon S3.
10
+ # @since 0.1.0
11
+ class UploadFile < PdfMage::Workers::Base
12
+ def perform(pdf_id, callback_url = nil, meta = nil)
13
+ validate_aws_config!
14
+
15
+ s3 = Aws::S3::Resource.new(
16
+ access_key_id: CONFIG.aws_account_key,
17
+ region: CONFIG.aws_account_region,
18
+ secret_access_key: CONFIG.aws_account_secret
19
+ )
20
+
21
+ obj = s3.bucket(CONFIG.aws_account_bucket).object(pdf_id)
22
+ obj.upload_file(pdf_filename(pdf_id))
23
+ pdf_url = obj.presigned_url(:get, expires_in: CONFIG.aws_presigned_url_duration)
24
+
25
+ `rm #{pdf_filename(pdf_id)}` if CONFIG.delete_file_on_upload
26
+ PdfMage::Workers::SendWebhook.perform_async(pdf_url, callback_url, meta) if string_exists?(callback_url)
27
+ end
28
+
29
+ private
30
+
31
+ # Checks for the present of all necessary AWS config options.
32
+ def validate_aws_config!
33
+ unless string_exists?(CONFIG.aws_account_key)
34
+ raise ArgumentError, 'You must define aws_account_key in your config file to upload PDFs.'
35
+ end
36
+
37
+ unless string_exists?(CONFIG.aws_account_secret)
38
+ raise ArgumentError, 'You must define aws_account_secret in your config file to upload PDFs.'
39
+ end
40
+
41
+ unless string_exists?(CONFIG.aws_account_region)
42
+ raise ArgumentError, 'You must define aws_account_region in your config file to upload PDFs.'
43
+ end
44
+
45
+ unless string_exists?(CONFIG.aws_account_bucket)
46
+ raise ArgumentError, 'You must define aws_account_bucket in your config file to upload PDFs.'
47
+ end
48
+
49
+ if CONFIG.aws_presigned_url_duration.nil?
50
+ raise ArgumentError, 'You must define aws_presigned_url_duration in your config file to upload PDFs.'
51
+ end
52
+
53
+ true
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ MODE = 'worker'
4
+
5
+ require 'pdf_mage/init'
6
+ require 'pdf_mage/workers/render_pdf'
7
+ require 'pdf_mage/workers/send_webhook'
8
+ require 'pdf_mage/workers/upload_file'
data/pdf_mage.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'pdf_mage/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'pdf_mage'
9
+ spec.version = PdfMage::VERSION
10
+ spec.authors = %w[Jeremy Haile Dean Papastrat]
11
+ spec.email = %w[jeremy@sideqik.com dean@sideqik.com]
12
+ spec.summary = 'A lightweight Ruby gem for rendering PDFs with Chrome Headless'
13
+ spec.homepage = 'https://github.com/sideqik/pdf-mage'
14
+ spec.license = 'MIT'
15
+ spec.executables = %w[pdf_mage]
16
+ spec.require_paths = %w[lib]
17
+ spec.bindir = 'bin'
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+
23
+ spec.add_dependency 'aws-sdk-s3', '~> 1'
24
+ spec.add_dependency 'redis', '~> 4.0'
25
+ spec.add_dependency 'sidekiq', '~> 5.1'
26
+ spec.add_dependency 'sinatra', '~> 2.0'
27
+ spec.add_dependency 'typhoeus', '~> 1.1'
28
+
29
+ spec.add_development_dependency 'bundler', '~> 1.16'
30
+ spec.add_development_dependency 'coveralls', '~> 0.8'
31
+ spec.add_development_dependency 'rake', '~> 10.0'
32
+ spec.add_development_dependency 'rspec', '~> 3.7'
33
+ spec.add_development_dependency 'rubocop', '~> 0.56'
34
+ end
metadata ADDED
@@ -0,0 +1,216 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pdf_mage
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy
8
+ - Haile
9
+ - Dean
10
+ - Papastrat
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2018-06-16 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: aws-sdk-s3
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - "~>"
21
+ - !ruby/object:Gem::Version
22
+ version: '1'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1'
30
+ - !ruby/object:Gem::Dependency
31
+ name: redis
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - "~>"
35
+ - !ruby/object:Gem::Version
36
+ version: '4.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '4.0'
44
+ - !ruby/object:Gem::Dependency
45
+ name: sidekiq
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - "~>"
49
+ - !ruby/object:Gem::Version
50
+ version: '5.1'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '5.1'
58
+ - !ruby/object:Gem::Dependency
59
+ name: sinatra
60
+ requirement: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - "~>"
63
+ - !ruby/object:Gem::Version
64
+ version: '2.0'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - "~>"
70
+ - !ruby/object:Gem::Version
71
+ version: '2.0'
72
+ - !ruby/object:Gem::Dependency
73
+ name: typhoeus
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - "~>"
77
+ - !ruby/object:Gem::Version
78
+ version: '1.1'
79
+ type: :runtime
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: '1.1'
86
+ - !ruby/object:Gem::Dependency
87
+ name: bundler
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '1.16'
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '1.16'
100
+ - !ruby/object:Gem::Dependency
101
+ name: coveralls
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - "~>"
105
+ - !ruby/object:Gem::Version
106
+ version: '0.8'
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '0.8'
114
+ - !ruby/object:Gem::Dependency
115
+ name: rake
116
+ requirement: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: '10.0'
121
+ type: :development
122
+ prerelease: false
123
+ version_requirements: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - "~>"
126
+ - !ruby/object:Gem::Version
127
+ version: '10.0'
128
+ - !ruby/object:Gem::Dependency
129
+ name: rspec
130
+ requirement: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - "~>"
133
+ - !ruby/object:Gem::Version
134
+ version: '3.7'
135
+ type: :development
136
+ prerelease: false
137
+ version_requirements: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - "~>"
140
+ - !ruby/object:Gem::Version
141
+ version: '3.7'
142
+ - !ruby/object:Gem::Dependency
143
+ name: rubocop
144
+ requirement: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - "~>"
147
+ - !ruby/object:Gem::Version
148
+ version: '0.56'
149
+ type: :development
150
+ prerelease: false
151
+ version_requirements: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - "~>"
154
+ - !ruby/object:Gem::Version
155
+ version: '0.56'
156
+ description:
157
+ email:
158
+ - jeremy@sideqik.com
159
+ - dean@sideqik.com
160
+ executables:
161
+ - pdf_mage
162
+ extensions: []
163
+ extra_rdoc_files: []
164
+ files:
165
+ - ".coveralls.yml"
166
+ - ".gitignore"
167
+ - ".rspec"
168
+ - ".rubocop.yml"
169
+ - ".travis.yml"
170
+ - CODE_OF_CONDUCT.md
171
+ - Gemfile
172
+ - Gemfile.lock
173
+ - LICENSE.txt
174
+ - README.md
175
+ - Rakefile
176
+ - bin/console
177
+ - bin/pdf_mage
178
+ - bin/setup
179
+ - bin/sidekiq
180
+ - lib/pdf_mage.rb
181
+ - lib/pdf_mage/.DS_Store
182
+ - lib/pdf_mage/api/app.rb
183
+ - lib/pdf_mage/config.yml
184
+ - lib/pdf_mage/init.rb
185
+ - lib/pdf_mage/version.rb
186
+ - lib/pdf_mage/workers/base.rb
187
+ - lib/pdf_mage/workers/render_pdf.rb
188
+ - lib/pdf_mage/workers/send_webhook.rb
189
+ - lib/pdf_mage/workers/upload_file.rb
190
+ - lib/pdf_mage_workers.rb
191
+ - pdf_mage.gemspec
192
+ homepage: https://github.com/sideqik/pdf-mage
193
+ licenses:
194
+ - MIT
195
+ metadata: {}
196
+ post_install_message:
197
+ rdoc_options: []
198
+ require_paths:
199
+ - lib
200
+ required_ruby_version: !ruby/object:Gem::Requirement
201
+ requirements:
202
+ - - ">="
203
+ - !ruby/object:Gem::Version
204
+ version: '0'
205
+ required_rubygems_version: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - ">="
208
+ - !ruby/object:Gem::Version
209
+ version: '0'
210
+ requirements: []
211
+ rubyforge_project:
212
+ rubygems_version: 2.7.4
213
+ signing_key:
214
+ specification_version: 4
215
+ summary: A lightweight Ruby gem for rendering PDFs with Chrome Headless
216
+ test_files: []