jekyll-paspagon 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +7 -0
- data/README.md +175 -0
- data/lib/jekyll/commands/paspagon.rb +116 -0
- data/lib/jekyll-paspagon/config.rb +67 -0
- data/lib/jekyll-paspagon/hooks.rb +180 -0
- data/lib/jekyll-paspagon.rb +3 -0
- metadata +106 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2d9c1cbf4a393ffc471f25e509d78e1ac3a681f3
|
4
|
+
data.tar.gz: eed3f8f170893e1f48ee441e495356d838b3f845
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9c7badcc45eba408df2b4e04576837e25c2d9bcda364026acb2350b9114e6709d9aac523a0c5d5302964205a5f80b14483e82c3632dd91b2387b51ecd2521e9c
|
7
|
+
data.tar.gz: a5c921004ed08b4f81802c8ddc9f1e494f048386507971d2f27c4f3ab02e710c7c6687ac1a40dfda3236968c588046865d6fbd0319bae49fa55b5a816a08de16
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright © 2016 Krzysztof Jurewicz
|
2
|
+
|
3
|
+
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:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
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.
|
data/README.md
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
# jekyll-paspagon
|
2
|
+
|
3
|
+
This [Jekyll](http://jekyllrb.com) plugin allows you to sell blog posts in various formats (HTML, EPUB, AZW3, MOBI, PDF and more) for [Bitcoin](https://bitcoin.org) and [BlackCoin](http://blackcoin.co), using Amazon S3 and [Paspagon](http://paspagon.com).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
### Install dependencies
|
8
|
+
|
9
|
+
If you want to sell posts in formats other than HTML, [install Pandoc](http://pandoc.org/installing.html). If you want to sell MOBI, AZW3 or PDF files, install [Calibre](https://calibre-ebook.com/).
|
10
|
+
|
11
|
+
### Install plugin
|
12
|
+
|
13
|
+
Add the `jekyll-paspagon` gem to the `:jekyll_plugins` group in your `Gemfile`:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
group :jekyll_plugins do
|
17
|
+
gem 'jekyll-paspagon', '~>1'
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
### Configure S3 bucket
|
22
|
+
|
23
|
+
Create a S3 bucket in the `s3-us-west-2` region (Oregon) and [grant Paspagon read access to it](http://paspagon.com/terms-seller/#step-1-grant-us-read-access-to-your-bucket), as described in Paspagon’s terms of service.
|
24
|
+
|
25
|
+
### Configure Paspagon
|
26
|
+
|
27
|
+
To your `_config.yml`, add a section adhering to the following example:
|
28
|
+
|
29
|
+
```yaml
|
30
|
+
paspagon:
|
31
|
+
# By making this entry, you indicate that you accept Paspagon’s terms of
|
32
|
+
# service. Be sure that you’ve actually read these terms!
|
33
|
+
accept-terms: https://github.com/Paspagon/paspagon.github.io/blob/master/terms-seller.md
|
34
|
+
buckets:
|
35
|
+
your-bucket-name:
|
36
|
+
seller:
|
37
|
+
country-code: PL
|
38
|
+
email: john.doe@example.com # Email is optional
|
39
|
+
payment:
|
40
|
+
# This section provides default values that you can override for each post.
|
41
|
+
#
|
42
|
+
# You can specify prices in BTC, BLK, XAU (troy ounce of gold) and some
|
43
|
+
# other currencies (see Paspagon’s terms of service for a complete list).
|
44
|
+
# (Paspagon takes only one price into account).
|
45
|
+
price:
|
46
|
+
USD: 3
|
47
|
+
address:
|
48
|
+
BTC: 1your-address
|
49
|
+
BLK: Byour-address
|
50
|
+
# Time (in seconds) after which download link will expire.
|
51
|
+
link-expiration-time: 600
|
52
|
+
# A name of a bucket which will be used to store S3 request logs.
|
53
|
+
logging_bucket: paspagon-logs-foo
|
54
|
+
```
|
55
|
+
|
56
|
+
### Set default post configuration
|
57
|
+
|
58
|
+
Add the bucket and formats configuration to the `default` section in your `_config.yml`. This is optional, but handy.
|
59
|
+
|
60
|
+
```yaml
|
61
|
+
defaults:
|
62
|
+
- scope:
|
63
|
+
path: ""
|
64
|
+
values:
|
65
|
+
formats:
|
66
|
+
# This section provides default values that you can override for each post.
|
67
|
+
html:
|
68
|
+
paid_after: 15 # HTML version will be paid 15 days after publication.
|
69
|
+
pdf:
|
70
|
+
content_disposition: attachment
|
71
|
+
content_type: application/pdf
|
72
|
+
paid_before: 2 # PDF version will be paid for the first two days.
|
73
|
+
epub: {} # EPUB version will be paid from the beginning.
|
74
|
+
bucket: your-bucket-name
|
75
|
+
```
|
76
|
+
|
77
|
+
Caveat: “default” values in Jekyll are actually not default values that can be simply overridden. Instead they get “deep merged” into post variables. If you want to unset a specific nested value in the `formats` hash, set it to `null`.
|
78
|
+
|
79
|
+
### Change post template
|
80
|
+
|
81
|
+
Inform your readers that you offer alternate post formats by modifying the post template. In a simple form the relevant fragment may look like this:
|
82
|
+
|
83
|
+
```liquid
|
84
|
+
{% if page.formats %}
|
85
|
+
<p>Available formats:
|
86
|
+
{% for format in page.format_array %}
|
87
|
+
<a href="{{ format.full_url }}">{{ format.name }}</a>{% unless forloop.last %},{% endunless %}
|
88
|
+
{% endfor %}
|
89
|
+
</p>
|
90
|
+
{% endif %}
|
91
|
+
```
|
92
|
+
|
93
|
+
A more sophisticated example, which uses the `page.excerpt_only` variable (set only when HTML is a paid format and a summary is being rendered):
|
94
|
+
|
95
|
+
```liquid
|
96
|
+
{% if page.formats %}
|
97
|
+
<p>
|
98
|
+
{% if page.excerpt_only %}
|
99
|
+
Available formats:
|
100
|
+
{% else %}
|
101
|
+
Alternate formats:
|
102
|
+
{% endif %}
|
103
|
+
{% for format in page.format_array %}
|
104
|
+
{% if format.name != 'HTML' or page.excerpt_only %}
|
105
|
+
<a {% if format.paid %}class="paid" {% endif %}href="{{ format.full_url }}">{{ format.name }}</a>{% unless forloop.last %},{% endunless %}
|
106
|
+
{% endif %}
|
107
|
+
{% endfor %}
|
108
|
+
</p>
|
109
|
+
{% endif %}
|
110
|
+
```
|
111
|
+
|
112
|
+
### Change feed template
|
113
|
+
|
114
|
+
If you provide a RSS/Atom feed, you may want to ensure that it contains only post excerpts instead of complete contents. Typically, you will need to change `{{ post.content | xml_escape }}` to `{{ post.excerpt | xml_escape }}` in your `feed.xml` template.
|
115
|
+
|
116
|
+
### Generate full URLs everywhere
|
117
|
+
|
118
|
+
If you’re going to sell HTML versions of your posts (which will be hosted on a different domain), you should ensure that links on your website contain the domain. The easiest way to do it is to remove all occurences of `site.url` from the templates (usually in `feed.xml` and `_includes/head.html`) and add the domain to the `baseurl` setting.
|
119
|
+
|
120
|
+
## Usage
|
121
|
+
|
122
|
+
After doing the steps above, paid versions of your posts will be generated automatically.
|
123
|
+
|
124
|
+
If you specify thresholds like `paid_before`, you will need to run `jekyll build` again after reaching them.
|
125
|
+
|
126
|
+
You may override formats, buckets, prices and payment addresses for each post by putting the relevant data in the YAML front matter:
|
127
|
+
|
128
|
+
```yaml
|
129
|
+
layout: post
|
130
|
+
title: Foo
|
131
|
+
payment:
|
132
|
+
price:
|
133
|
+
USD: null
|
134
|
+
XAU: 3
|
135
|
+
address:
|
136
|
+
bitcoin: 1Foo
|
137
|
+
bucket: foo
|
138
|
+
formats:
|
139
|
+
epub:
|
140
|
+
paid_after: 15
|
141
|
+
pdf:
|
142
|
+
# Setting this to null means that a post will be paid from the beginning.
|
143
|
+
# If you want a format to be free instead, set this to 0.
|
144
|
+
paid_before: null
|
145
|
+
azw3: {}
|
146
|
+
```
|
147
|
+
|
148
|
+
Two Jekyll subcommands are available to synchronize paid content with Amazon S3. You need to [specify AWS credentials](http://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs) beforehand.
|
149
|
+
|
150
|
+
### paspagon_prepare
|
151
|
+
|
152
|
+
This command creates necessary buckets, configures logging and log expiration (to 90 days) and sets permissions for Paspagon. It typically needs to be executed only once, after configuring Paspagon.
|
153
|
+
|
154
|
+
### paspagon_sync
|
155
|
+
|
156
|
+
This command uploads missing or updated paid files to S3 and removes ones which are not present locally. It should be ran each time a blog content update is deployed.
|
157
|
+
|
158
|
+
## Markdown compatibility
|
159
|
+
|
160
|
+
jekyll-paspagon uses Pandoc to generate formats other than HTML. The default input format is `markdown_github-hard_line_breaks`. It may be impacted by some site settings like Kramdown’s `hard_wrap`, but the most reliable it to set it explicitly in `_config.yml`:
|
161
|
+
|
162
|
+
```yaml
|
163
|
+
pandoc:
|
164
|
+
input: markdown_phpextra-fenced_code_blocks+strikeout
|
165
|
+
```
|
166
|
+
|
167
|
+
For more information, refer to [Pandoc’s documentation on Markdown](http://pandoc.org/README.html#pandocs-markdown).
|
168
|
+
|
169
|
+
## Versioning
|
170
|
+
|
171
|
+
This project uses [semantic versioning](http://semver.org/).
|
172
|
+
|
173
|
+
## License
|
174
|
+
|
175
|
+
This software is licensed under [the MIT License](LICENSE).
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'aws-sdk-resources'
|
3
|
+
require 'ffi-xattr'
|
4
|
+
require 'jekyll-paspagon/config'
|
5
|
+
|
6
|
+
module Jekyll
|
7
|
+
module Commands
|
8
|
+
class Paspagon < Command
|
9
|
+
class << self
|
10
|
+
def init_with_program(jekyll)
|
11
|
+
client = Aws::S3::Client.new(region: 'us-west-2')
|
12
|
+
jekyll.command(:paspagon_prepare) do |c|
|
13
|
+
c.syntax 'paspagon_prepare'
|
14
|
+
c.description 'Create and configure S3 buckets for Paspagon'
|
15
|
+
|
16
|
+
c.action do |_, options|
|
17
|
+
site_options = configuration_from_options(options)
|
18
|
+
site = Jekyll::Site.new(site_options)
|
19
|
+
config = PaspagonConfig.new(site)
|
20
|
+
|
21
|
+
logging_bucket_name = config.logging_bucket_name
|
22
|
+
logging_bucket = ensure_logging_bucket(logging_bucket_name, client) if logging_bucket_name
|
23
|
+
config.buckets.keys.each do |bucket_name|
|
24
|
+
prepare_bucket(bucket_name, client, logging_bucket)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
jekyll.command(:paspagon_sync) do |c|
|
30
|
+
c.syntax 'paspagon_sync'
|
31
|
+
c.description 'Sync paid files with S3'
|
32
|
+
|
33
|
+
c.action do |_, options|
|
34
|
+
site_options = configuration_from_options(options)
|
35
|
+
site = Jekyll::Site.new(site_options)
|
36
|
+
config = PaspagonConfig.new(site)
|
37
|
+
|
38
|
+
paid_dir = config.paid_dest_dir
|
39
|
+
Dir["#{paid_dir}/*/"].each do |dir|
|
40
|
+
sync_path(dir, client)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def prepare_bucket(name, client, logging_bucket)
|
47
|
+
bucket = Aws::S3::Bucket.new(name, client: client)
|
48
|
+
bucket.create unless bucket.exists?
|
49
|
+
policy = <<-JSON
|
50
|
+
{
|
51
|
+
"Version": "2012-10-17",
|
52
|
+
"Statement": {
|
53
|
+
"Resource": "arn:aws:s3:::#{name}/*",
|
54
|
+
"Sid": "PaspagonAllow",
|
55
|
+
"Effect": "Allow",
|
56
|
+
"Principal": {"AWS": "154072225287"},
|
57
|
+
"Action": ["s3:GetObject"]
|
58
|
+
}
|
59
|
+
}
|
60
|
+
JSON
|
61
|
+
client.put_bucket_policy(bucket: name, policy: policy)
|
62
|
+
logging_status =
|
63
|
+
{logging_enabled:
|
64
|
+
{target_bucket: logging_bucket.name,
|
65
|
+
target_prefix: "#{name}/"}}
|
66
|
+
client.put_bucket_logging(bucket: name, bucket_logging_status: logging_status)
|
67
|
+
end
|
68
|
+
|
69
|
+
def ensure_logging_bucket(name, client)
|
70
|
+
bucket = Aws::S3::Bucket.new(name: name, client: client)
|
71
|
+
bucket.create unless bucket.exists?
|
72
|
+
lifecycle_configuration =
|
73
|
+
{rules:
|
74
|
+
[{expiration:
|
75
|
+
{days: 90},
|
76
|
+
prefix: '',
|
77
|
+
status: 'Enabled'
|
78
|
+
}]}
|
79
|
+
client.put_bucket_lifecycle_configuration(bucket: name, lifecycle_configuration: lifecycle_configuration)
|
80
|
+
client.put_bucket_acl(bucket: name, acl: 'log-delivery-write')
|
81
|
+
bucket
|
82
|
+
end
|
83
|
+
|
84
|
+
def sync_path(path, client)
|
85
|
+
Dir.chdir(path) do
|
86
|
+
local_files =
|
87
|
+
Dir.glob('**/*').select { |p| File.file?(p) }.map { |p| [p, [File.mtime(p), File.ctime(p)].max] }.to_h
|
88
|
+
|
89
|
+
bucket_name = File.basename(path)
|
90
|
+
bucket = Aws::S3::Bucket.new(bucket_name, client: client)
|
91
|
+
cloud_files = bucket.objects.map { |o| [o.key, o.last_modified] }.to_h
|
92
|
+
|
93
|
+
to_delete = (cloud_files.keys - local_files.keys)
|
94
|
+
unless to_delete.empty?
|
95
|
+
puts('Deleting objects:')
|
96
|
+
to_delete.each do |p|
|
97
|
+
puts("\t#{p}")
|
98
|
+
end
|
99
|
+
bucket.delete_objects(delete: {objects: to_delete.map { |p| {key: p} }})
|
100
|
+
end
|
101
|
+
|
102
|
+
to_upload = local_files.keys.select { |p| !cloud_files[p] || local_files[p] > cloud_files[p] }
|
103
|
+
to_upload.each do |p|
|
104
|
+
puts("Uploading #{p} to bucket #{bucket_name}…")
|
105
|
+
xattr = Xattr.new(p)
|
106
|
+
metadata = xattr.as_json.select { |k, _| k.start_with?('user.') }.map { |k, v| [k.sub(/^user./, ''), v] }.to_h
|
107
|
+
bucket.put_object(body: File.open(p),
|
108
|
+
metadata: metadata,
|
109
|
+
key: p)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
class PaspagonConfig
|
4
|
+
attr_reader :buckets, :formats, :full_config
|
5
|
+
|
6
|
+
def initialize(site)
|
7
|
+
@site = site
|
8
|
+
@full_config = site.config['paspagon'] || {}
|
9
|
+
@terms_accepted = @full_config['accept-terms'] == 'https://github.com/Paspagon/paspagon.github.io/blob/master/terms-seller.md'
|
10
|
+
raise 'Paspagon terms not accepted' unless @terms_accepted
|
11
|
+
@buckets = @full_config['buckets'] || {}
|
12
|
+
@formats = @full_config['formats'] || {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def post_config_complete(post)
|
16
|
+
payment_config = full_payment_config post
|
17
|
+
prices(payment_config) && addresses(payment_config)
|
18
|
+
end
|
19
|
+
|
20
|
+
def bucket_name(post)
|
21
|
+
bucket = post.data['bucket']
|
22
|
+
raise 'Bucket not specified for post' unless bucket
|
23
|
+
raise "Bucket not found: #{bucket}" unless @buckets.key?(bucket)
|
24
|
+
bucket
|
25
|
+
end
|
26
|
+
|
27
|
+
def format_configs(post)
|
28
|
+
@formats.merge(post.data['formats'] || {})
|
29
|
+
end
|
30
|
+
|
31
|
+
def paid_dest_dir
|
32
|
+
File.join(@site.dest, '../_paid')
|
33
|
+
end
|
34
|
+
|
35
|
+
def bucket_dest_dir(bucket_name)
|
36
|
+
File.join(paid_dest_dir, bucket_name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def write_buckets_config
|
40
|
+
@buckets.each do |bucket_name, bucket_hash|
|
41
|
+
bucket_config = bucket_hash.update('accept-terms' => @full_config['accept-terms'])
|
42
|
+
ini = hash_to_ini bucket_config
|
43
|
+
dest = File.join(bucket_dest_dir(bucket_name), 'paspagon.ini')
|
44
|
+
ini_hash = Digest::SHA256.digest(ini)
|
45
|
+
next if File.exist?(dest) && Digest::SHA256.file(dest).digest == ini_hash
|
46
|
+
File.open(dest, 'wb') do |f|
|
47
|
+
f.write(ini)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def logging_bucket_name
|
53
|
+
@full_config['logging_bucket']
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def hash_to_ini(hash, section = nil, section_nested = nil)
|
58
|
+
sections, entries = hash.partition { |_, v| v.is_a? Hash }
|
59
|
+
lines = []
|
60
|
+
lines += ["[#{section}]"] if section && !section_nested
|
61
|
+
section_prefix = section_nested ? "#{section}-" : ''
|
62
|
+
lines += entries.map { |e| section_prefix + e.join(' = ') }
|
63
|
+
lines += sections.map do |inner_section, inner_section_entries|
|
64
|
+
hash_to_ini(inner_section_entries, section_prefix + inner_section, section)
|
65
|
+
end
|
66
|
+
lines.flatten.join("\n")
|
67
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'fileutils'
|
3
|
+
require 'tmpdir'
|
4
|
+
require 'ffi-xattr'
|
5
|
+
require 'jekyll-paspagon/config'
|
6
|
+
|
7
|
+
Jekyll::Hooks.register :site, :pre_render do |site|
|
8
|
+
config = PaspagonConfig.new(site)
|
9
|
+
site.exclude << '_paid' unless site.exclude.include?('_paid')
|
10
|
+
site.posts.docs.each do |post|
|
11
|
+
post.data['formats'] ||= {}
|
12
|
+
# Jekyll does not do a simple merge of default values with post values, so
|
13
|
+
# we cannot unset default formats in an ordinary way. Instead, we reject null values.
|
14
|
+
post.data['formats'].reject! { |_, v| !v }
|
15
|
+
prices = post.data['price'] || {}
|
16
|
+
addresses = post.data['address'] || {}
|
17
|
+
link_expiration_time = post.data['link-expiration-time'] || nil
|
18
|
+
config.format_configs(post).each do |format, format_config|
|
19
|
+
paid = format_paid? post, format_config
|
20
|
+
format_dest_dir =
|
21
|
+
if paid
|
22
|
+
bucket_name = config.bucket_name post
|
23
|
+
File.dirname(File.join(config.bucket_dest_dir(bucket_name), post.url))
|
24
|
+
else
|
25
|
+
File.dirname(File.join(site.dest, post.url))
|
26
|
+
end
|
27
|
+
post_basename = File.basename(post.url)
|
28
|
+
format_filename = "#{post_basename}.#{format}"
|
29
|
+
format_dest = File.join(format_dest_dir, format_filename)
|
30
|
+
format_url_ending =
|
31
|
+
if format == 'html' && !paid
|
32
|
+
post.url
|
33
|
+
else
|
34
|
+
File.join(File.dirname(post.url), format_filename)
|
35
|
+
end
|
36
|
+
format_url =
|
37
|
+
if paid
|
38
|
+
bucket_name = config.bucket_name post
|
39
|
+
"https://s3-us-west-2.paspagon.com/#{bucket_name}#{format_url_ending}"
|
40
|
+
else
|
41
|
+
"#{site.config['url']}#{site.config['baseurl']}#{format_url_ending}"
|
42
|
+
end
|
43
|
+
|
44
|
+
format_info =
|
45
|
+
{'paid' => paid,
|
46
|
+
'post' => post,
|
47
|
+
'path' => format_dest,
|
48
|
+
'full_url' => format_url,
|
49
|
+
'link_expiration_time' => link_expiration_time,
|
50
|
+
'prices' => prices,
|
51
|
+
'name' => format.upcase,
|
52
|
+
'addresses' => addresses}
|
53
|
+
post.data['formats'][format].merge! format_info
|
54
|
+
post.data['format_array'] = post.data['formats'].map { |_, v| v }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
config.write_buckets_config
|
59
|
+
end
|
60
|
+
|
61
|
+
Jekyll::Hooks.register :posts, :post_write do |post|
|
62
|
+
unless post.data['excerpt_only']
|
63
|
+
post.data['formats'].each do |format, format_config|
|
64
|
+
puts("Generated #{format_config['path']}.") if maybe_generate_doc(post, format)
|
65
|
+
attrs = format_xattrs(format_config)
|
66
|
+
sync_payment_attributes(format_config['path'], attrs)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def format_xattrs(format_config)
|
72
|
+
attrs = {}
|
73
|
+
format_config['prices'].each do |currency, price|
|
74
|
+
attrs["user.x-amz-meta-price-#{currency}"] = price if price
|
75
|
+
end
|
76
|
+
format_config['addresses'].each do |currency, address|
|
77
|
+
attrs["user.x-amz-meta-address-#{currency}"] = address if address
|
78
|
+
end
|
79
|
+
attrs['user.x-amz-meta-link-expiration-time'] = format_config['link_expiration_time'] if format_config['link_expiration_time']
|
80
|
+
attrs['user.content-disposition'] = format_config['content_disposition'] if format_config['content_disposition']
|
81
|
+
attrs['user.content-type'] = format_config['content_type'] if format_config['content_type']
|
82
|
+
attrs
|
83
|
+
end
|
84
|
+
|
85
|
+
def sync_payment_attributes(path, attrs)
|
86
|
+
xattr = Xattr.new(path)
|
87
|
+
old = xattr.list.select { |a| a.start_with?('user.') }
|
88
|
+
new = attrs.keys
|
89
|
+
(old - new).each do |a|
|
90
|
+
xattr.remove a
|
91
|
+
end
|
92
|
+
new.each do |a|
|
93
|
+
xattr[a] = attrs[a] unless xattr[a] == attrs[a].to_s
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def maybe_generate_doc(post, format)
|
98
|
+
format_config = post.data['formats'][format]
|
99
|
+
dest_path = format_config['path']
|
100
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
101
|
+
if File.exist?(dest_path)
|
102
|
+
source_path = post.path
|
103
|
+
if File.ctime(source_path) > File.ctime(dest_path)
|
104
|
+
temp_path = File.join(Dir.tmpdir, Digest::SHA256.hexdigest(dest_path) + '.' + format)
|
105
|
+
generate_doc(post, format, format_config, temp_path)
|
106
|
+
temp_hash = Digest::SHA256.file(temp_path)
|
107
|
+
dest_hash = Digest::SHA256.file(dest_path)
|
108
|
+
if temp_hash == dest_hash
|
109
|
+
# Unfortunately, this check usually doesn’t yield desired effect, as Pandoc’s conversion to EPUB is impure.
|
110
|
+
File.delete(temp_path)
|
111
|
+
false
|
112
|
+
else
|
113
|
+
FileUtils.mv(temp_path, dest_path)
|
114
|
+
true
|
115
|
+
end
|
116
|
+
else
|
117
|
+
false
|
118
|
+
end
|
119
|
+
else
|
120
|
+
generate_doc(post, format, format_config, dest_path)
|
121
|
+
true
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def generate_doc(post, format, format_config, dest_path)
|
126
|
+
case format
|
127
|
+
when 'html'
|
128
|
+
if format_config['paid']
|
129
|
+
free_dest = post.destination
|
130
|
+
system("mv #{free_dest} #{dest_path}")
|
131
|
+
post.content = post.data['excerpt'].output
|
132
|
+
post.data['excerpt_only'] = true
|
133
|
+
post.output = Jekyll::Renderer.new(post.site, post, post.site.site_payload).run
|
134
|
+
# We don’t use post.write method to not trigger infinite hook recursion.
|
135
|
+
File.open(free_dest, 'wb') do |f|
|
136
|
+
f.write(post.output)
|
137
|
+
end
|
138
|
+
end # If HTML format is free, then it is already generated, so we don’t need to do anything.
|
139
|
+
else
|
140
|
+
command = generation_command(post, format, format_config, dest_path)
|
141
|
+
raise "Failed to execute: #{command}" unless system command
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def generation_command(post, format, _format_config, dest_path)
|
146
|
+
input_format = pandoc_input_format(post.site)
|
147
|
+
case format
|
148
|
+
when 'azw3', 'mobi', 'pdf'
|
149
|
+
intermediary_path = File.join(Dir.tmpdir, Digest::SHA256.hexdigest(dest_path) + '.epub')
|
150
|
+
"pandoc #{post.path} -f #{input_format} -t epub -o #{intermediary_path} && ebook-convert #{intermediary_path} #{dest_path} > /dev/null && rm #{intermediary_path}"
|
151
|
+
else
|
152
|
+
"pandoc #{post.path} -f #{input_format} -t #{format} -o #{dest_path}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def pandoc_input_format(site)
|
157
|
+
(site.config['pandoc'] || {})['input'] ||
|
158
|
+
begin
|
159
|
+
kramdown = site.config['kramdown'] || {}
|
160
|
+
format = 'markdown_github'
|
161
|
+
format += '-hard_line_breaks' unless kramdown['hard_wrap']
|
162
|
+
format
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def format_paid?(post, format_config)
|
167
|
+
too_late =
|
168
|
+
if format_config['paid_before']
|
169
|
+
Date.today - Date.parse(post.date.to_s) >= format_config['paid_before'].to_i
|
170
|
+
else
|
171
|
+
false
|
172
|
+
end
|
173
|
+
too_soon =
|
174
|
+
if format_config['paid_after']
|
175
|
+
Date.today - Date.parse(post.date.to_s) <= format_config['paid_after'].to_i
|
176
|
+
else
|
177
|
+
false
|
178
|
+
end
|
179
|
+
!too_late && !too_soon
|
180
|
+
end
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jekyll-paspagon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Krzysztof Jurewicz
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ffi-xattr
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.1'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.1.2
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.1'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.1.2
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: aws-sdk
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rubocop
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0.38'
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 0.38.0
|
57
|
+
type: :development
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - "~>"
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0.38'
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 0.38.0
|
67
|
+
description: Sell your Jekyll posts in various formats for cryptocurrencies
|
68
|
+
email: krzysztof.jurewicz@gmail.com
|
69
|
+
executables: []
|
70
|
+
extensions: []
|
71
|
+
extra_rdoc_files:
|
72
|
+
- README.md
|
73
|
+
- LICENSE
|
74
|
+
files:
|
75
|
+
- LICENSE
|
76
|
+
- README.md
|
77
|
+
- lib/jekyll-paspagon.rb
|
78
|
+
- lib/jekyll-paspagon/config.rb
|
79
|
+
- lib/jekyll-paspagon/hooks.rb
|
80
|
+
- lib/jekyll/commands/paspagon.rb
|
81
|
+
homepage: https://github.com/KrzysiekJ/jekyll-paspagon
|
82
|
+
licenses:
|
83
|
+
- MIT
|
84
|
+
metadata: {}
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 2.2.2
|
102
|
+
signing_key:
|
103
|
+
specification_version: 4
|
104
|
+
summary: Sell your Jekyll posts
|
105
|
+
test_files: []
|
106
|
+
has_rdoc:
|