fronde 0.3.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/fronde +15 -30
- data/lib/ext/nil_time.rb +25 -0
- data/lib/ext/r18n.rb +37 -0
- data/lib/ext/time.rb +39 -0
- data/lib/ext/time_no_time.rb +23 -0
- data/lib/fronde/cli/commands.rb +97 -104
- data/lib/fronde/cli/data/Rakefile +8 -0
- data/lib/fronde/cli/data/config.yml +13 -0
- data/lib/fronde/cli/data/gitignore +6 -0
- data/lib/fronde/cli/data/zsh_completion +37 -0
- data/lib/fronde/cli/helpers.rb +55 -0
- data/lib/fronde/cli/opt_parse.rb +140 -0
- data/lib/fronde/cli/throbber.rb +110 -0
- data/lib/fronde/cli.rb +42 -42
- data/lib/fronde/config/data/org-config.el +25 -0
- data/lib/fronde/config/data/ox-fronde.el +158 -0
- data/lib/fronde/config/data/themes/umaneti/css/htmlize.css +364 -0
- data/lib/fronde/config/data/themes/umaneti/css/style.css +250 -0
- data/lib/fronde/config/data/themes/umaneti/img/bottom.png +0 -0
- data/lib/fronde/config/data/themes/umaneti/img/content.png +0 -0
- data/lib/fronde/config/data/themes/umaneti/img/tic.png +0 -0
- data/lib/fronde/config/data/themes/umaneti/img/top.png +0 -0
- data/lib/fronde/config/helpers.rb +62 -0
- data/lib/fronde/config/lisp.rb +80 -0
- data/lib/fronde/config.rb +148 -98
- data/lib/fronde/emacs.rb +23 -20
- data/lib/fronde/index/atom_generator.rb +55 -66
- data/lib/fronde/index/data/all_tags.org +19 -0
- data/lib/fronde/index/data/template.org +26 -0
- data/lib/fronde/index/data/template.xml +37 -0
- data/lib/fronde/index/org_generator.rb +72 -88
- data/lib/fronde/index.rb +57 -86
- data/lib/fronde/org/file.rb +299 -0
- data/lib/fronde/org/file_extracter.rb +101 -0
- data/lib/fronde/org.rb +105 -0
- data/lib/fronde/preview.rb +43 -39
- data/lib/fronde/slug.rb +54 -0
- data/lib/fronde/source/gemini.rb +34 -0
- data/lib/fronde/source/html.rb +67 -0
- data/lib/fronde/source.rb +209 -0
- data/lib/fronde/sync/neocities.rb +220 -0
- data/lib/fronde/sync/rsync.rb +46 -0
- data/lib/fronde/sync.rb +32 -0
- data/lib/fronde/templater.rb +101 -71
- data/lib/fronde/version.rb +1 -1
- data/lib/tasks/cli.rake +33 -0
- data/lib/tasks/org.rake +58 -43
- data/lib/tasks/site.rake +66 -31
- data/lib/tasks/sync.rake +37 -40
- data/lib/tasks/tags.rake +11 -7
- data/locales/en.yml +61 -14
- data/locales/fr.yml +69 -14
- metadata +77 -95
- data/lib/fronde/config/lisp_config.rb +0 -340
- data/lib/fronde/config/org-config.el +0 -19
- data/lib/fronde/config/ox-fronde.el +0 -121
- data/lib/fronde/org_file/class_methods.rb +0 -72
- data/lib/fronde/org_file/extracter.rb +0 -72
- data/lib/fronde/org_file/htmlizer.rb +0 -43
- data/lib/fronde/org_file.rb +0 -298
- data/lib/fronde/utils.rb +0 -229
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fronde
|
4
|
+
class Source
|
5
|
+
# Specific settings for HTML {Fronde::Source}
|
6
|
+
class Html < Source
|
7
|
+
def org_config
|
8
|
+
config = super
|
9
|
+
config[0]['theme'] = @config['theme']
|
10
|
+
config
|
11
|
+
end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def org_default_postamble
|
15
|
+
<<~POSTAMBLE
|
16
|
+
<p><span class="author">#{R18n.t.fronde.org.postamble.written_by}</span>
|
17
|
+
#{R18n.t.fronde.org.postamble.with_emacs_html}</p>
|
18
|
+
<p class="date">#{R18n.t.fronde.org.postamble.last_modification}</p>
|
19
|
+
<p class="validation">%v</p>
|
20
|
+
POSTAMBLE
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def fill_in_specific_config
|
27
|
+
@config.merge!(
|
28
|
+
'type' => 'html', 'ext' => '.html', 'mime_type' => 'text/html',
|
29
|
+
'folder' => CONFIG.get('html_public_folder')
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def org_publish_options
|
34
|
+
if @config['is_blog']
|
35
|
+
@config['atom_feed'] = <<~ATOMFEED
|
36
|
+
<link rel="alternate" type="application/atom+xml" title="#{@config['title']}"
|
37
|
+
href="#{@config['domain']}#{public_absolute_path}feeds/index.xml" />
|
38
|
+
ATOMFEED
|
39
|
+
end
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
def org_default_options
|
44
|
+
defaults = {
|
45
|
+
'publishing-function' => 'org-html-publish-to-html',
|
46
|
+
'html-head-include-default-style' => 't',
|
47
|
+
'html-head-include-scripts' => 't',
|
48
|
+
'html-head' => '%F',
|
49
|
+
'html-postamble' => Html.org_default_postamble
|
50
|
+
}
|
51
|
+
return defaults if @config['theme'] == 'default'
|
52
|
+
|
53
|
+
defaults.merge(
|
54
|
+
'html-head-include-default-style' => 'nil',
|
55
|
+
'html-head-include-scripts' => 'nil',
|
56
|
+
'html-head' => <<~HTMLHEAD
|
57
|
+
<link rel="stylesheet" type="text/css" media="screen"
|
58
|
+
href="%h/assets/%o/css/style.css">
|
59
|
+
<link rel="stylesheet" type="text/css" media="screen"
|
60
|
+
href="%h/assets/%o/css/htmlize.css">
|
61
|
+
%F
|
62
|
+
HTMLHEAD
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fronde
|
4
|
+
# Wrapper for each possible project source.
|
5
|
+
# Must be subclassed by specific source type (like Gemini or HTML)
|
6
|
+
class Source
|
7
|
+
def initialize(source_config)
|
8
|
+
@config = {
|
9
|
+
'recursive' => true, 'is_blog' => false,
|
10
|
+
'domain' => CONFIG.get('domain'), 'atom_feed' => '',
|
11
|
+
'org-options' => {
|
12
|
+
'section-numbers' => 'nil', 'with-toc' => 'nil'
|
13
|
+
}
|
14
|
+
}.merge(source_config)
|
15
|
+
clean_config
|
16
|
+
org_publish_options
|
17
|
+
render_heading
|
18
|
+
end
|
19
|
+
|
20
|
+
def [](key)
|
21
|
+
@config[key]
|
22
|
+
end
|
23
|
+
|
24
|
+
def []=(key, value)
|
25
|
+
@config[key] = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def type
|
29
|
+
@config['type']
|
30
|
+
end
|
31
|
+
|
32
|
+
def recursive?
|
33
|
+
!!@config['recursive']
|
34
|
+
end
|
35
|
+
|
36
|
+
def blog?
|
37
|
+
!!@config['is_blog']
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_h
|
41
|
+
@config
|
42
|
+
end
|
43
|
+
|
44
|
+
def source_for?(file_name)
|
45
|
+
relative_file_path = file_name.delete_prefix "#{@config['path']}/"
|
46
|
+
# file_name does not begin with source path.
|
47
|
+
return false if relative_file_path == file_name
|
48
|
+
|
49
|
+
# Looks like a file at a deeper level, but current source is not
|
50
|
+
# recursive.
|
51
|
+
return false if relative_file_path.include?('/') && !recursive?
|
52
|
+
|
53
|
+
# We don’t check file if the file really exist as the current
|
54
|
+
# check may be done before the file is actually written.
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
def source_for(file_name)
|
59
|
+
relative_file_path = file_name.delete_prefix "#{publication_path}/"
|
60
|
+
# file_name does not begin with source path.
|
61
|
+
return nil if relative_file_path == file_name
|
62
|
+
|
63
|
+
# Looks like a file at a deeper level, but current source is not
|
64
|
+
# recursive.
|
65
|
+
return nil if relative_file_path.include?('/') && !recursive?
|
66
|
+
|
67
|
+
# Looks like a match. But does a source file for this one actually
|
68
|
+
# exists?
|
69
|
+
relative_source_path = relative_file_path.sub(
|
70
|
+
/#{@config['ext']}\z/, '.org'
|
71
|
+
)
|
72
|
+
source_path = File.join(@config['path'], relative_source_path)
|
73
|
+
return nil unless File.file?(source_path)
|
74
|
+
|
75
|
+
source_path
|
76
|
+
end
|
77
|
+
|
78
|
+
def target_for(file_name)
|
79
|
+
target = File.expand_path file_name
|
80
|
+
target.delete_prefix! "#{Dir.pwd}/"
|
81
|
+
target.sub!(/\.org\z/, @config['ext'])
|
82
|
+
project_relative_path = @config['path'].delete_prefix("#{Dir.pwd}/")
|
83
|
+
target.delete_prefix! "#{project_relative_path}/"
|
84
|
+
public_absolute_path + target
|
85
|
+
end
|
86
|
+
|
87
|
+
def exclude_file?(file_name)
|
88
|
+
# Obviously excluding index itself for blogs
|
89
|
+
return true if file_name == File.join(@config['path'], 'index.org')
|
90
|
+
|
91
|
+
exclusion_rules = @config['exclude']
|
92
|
+
return false unless exclusion_rules
|
93
|
+
|
94
|
+
file_name.match? exclusion_rules
|
95
|
+
end
|
96
|
+
|
97
|
+
def org_config
|
98
|
+
name = @config['name']
|
99
|
+
[{ 'name' => name, 'attributes' => org_project_config },
|
100
|
+
{ 'name' => "#{name}-assets", 'attributes' => org_assets_config }]
|
101
|
+
end
|
102
|
+
|
103
|
+
# Return the publication absolute path on file system.
|
104
|
+
#
|
105
|
+
# The returned string never end with a slash (/).
|
106
|
+
#
|
107
|
+
# Use {Fronde::Source#public_absolute_path} to get the absolute path
|
108
|
+
# of this project, as seen from a web browser.
|
109
|
+
#
|
110
|
+
# @return [String] the absolute path to the target dir of this project
|
111
|
+
def publication_path
|
112
|
+
return @config['publication_path'] if @config['publication_path']
|
113
|
+
|
114
|
+
publish_in = [File.expand_path(@config['folder']), @config['target']]
|
115
|
+
@config['publication_path'] = publish_in.join('/').delete_suffix('/')
|
116
|
+
end
|
117
|
+
|
118
|
+
# Return the absolute path as seen in User Agent.
|
119
|
+
#
|
120
|
+
# The returned string always end with a slash (/).
|
121
|
+
#
|
122
|
+
# Use {Fronde::Source#publication_path} to locate published file on
|
123
|
+
# the file system.
|
124
|
+
#
|
125
|
+
# @return [String] the absolute path to this project
|
126
|
+
def public_absolute_path
|
127
|
+
return @config['public_absolute_path'] if @config['public_absolute_path']
|
128
|
+
|
129
|
+
@config['public_absolute_path'] = "/#{@config['target']}/".squeeze('/')
|
130
|
+
end
|
131
|
+
|
132
|
+
class << self
|
133
|
+
def canonical_config(config)
|
134
|
+
config = { 'path' => config } if config.is_a?(String)
|
135
|
+
config['type'] ||= 'html'
|
136
|
+
config
|
137
|
+
end
|
138
|
+
|
139
|
+
def new_from_config(config)
|
140
|
+
klass_name = config['type'].capitalize
|
141
|
+
klass = Kernel.const_get("::Fronde::Source::#{klass_name}")
|
142
|
+
klass.new(config)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def clean_config
|
149
|
+
fill_in_specific_config
|
150
|
+
@config['name'] ||= @config['path'].sub(%r{^[.~]*/}, '').tr('/.', '-')
|
151
|
+
@config['title'] ||= @config['path']
|
152
|
+
@config['target'] ||= File.basename(@config['path']).delete_prefix '.'
|
153
|
+
@config['target'] = '' if @config['target'] == '.'
|
154
|
+
@config['path'] = File.expand_path(@config['path'])
|
155
|
+
@config['theme'] ||= CONFIG.get('theme', 'default')
|
156
|
+
# Blog are necessarily recursive to allow publication of tags and feeds
|
157
|
+
@config['recursive'] = true if @config['is_blog']
|
158
|
+
end
|
159
|
+
|
160
|
+
def org_publish_options
|
161
|
+
type = @config['type']
|
162
|
+
options = org_default_options.merge(
|
163
|
+
@config['org-options'],
|
164
|
+
CONFIG.get("org-#{type}", {}),
|
165
|
+
@config["org-#{type}"] || {}
|
166
|
+
)
|
167
|
+
@config['org-options'] = options
|
168
|
+
end
|
169
|
+
|
170
|
+
def render_heading
|
171
|
+
%w[head head-extra preamble postamble].each do |kind|
|
172
|
+
heading_key = "#{@config['type']}-#{kind}"
|
173
|
+
heading = @config.dig 'org-options', heading_key
|
174
|
+
next unless heading
|
175
|
+
|
176
|
+
@config['org-options'][heading_key] =
|
177
|
+
heading.gsub('%F', @config['atom_feed'])
|
178
|
+
.gsub('%h', @config['domain'])
|
179
|
+
.gsub('%o', @config['theme'])
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def org_project_config
|
184
|
+
attributes = {
|
185
|
+
'base-directory' => @config['path'],
|
186
|
+
'base-extension' => 'org',
|
187
|
+
'publishing-directory' => publication_path,
|
188
|
+
'recursive' => @config['recursive'],
|
189
|
+
'fronde-base-uri' => "#{@config['domain']}#{public_absolute_path}"
|
190
|
+
}.merge(@config['org-options'])
|
191
|
+
exclude = @config['exclude']
|
192
|
+
attributes['exclude'] = exclude if exclude
|
193
|
+
attributes.sort.to_h # Have lisp config sorted
|
194
|
+
end
|
195
|
+
|
196
|
+
def org_assets_config
|
197
|
+
{
|
198
|
+
'base-directory' => @config['path'],
|
199
|
+
'base-extension' => %w[gif jpg png svg pdf].join('\\\\|'),
|
200
|
+
'publishing-directory' => publication_path,
|
201
|
+
'publishing-function' => 'org-publish-attachment',
|
202
|
+
'recursive' => @config['recursive']
|
203
|
+
}
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
require_relative 'source/gemini'
|
209
|
+
require_relative 'source/html'
|
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'time'
|
5
|
+
require 'yaml'
|
6
|
+
require 'net/http'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'digest/sha1'
|
9
|
+
|
10
|
+
module Fronde
|
11
|
+
module Sync
|
12
|
+
# Everything needed to connect to neocities
|
13
|
+
class Neocities
|
14
|
+
PROTECTED_FILES = %w[index.html neocities.png not_found.html].freeze
|
15
|
+
|
16
|
+
def initialize(connection_spec, public_folder, verbose: false, &block)
|
17
|
+
@verbose = verbose
|
18
|
+
@endpoint = @website_name = @authorization = nil
|
19
|
+
extract_connection_details connection_spec
|
20
|
+
@public_folder = public_folder
|
21
|
+
@connection = init_connection
|
22
|
+
return unless block
|
23
|
+
|
24
|
+
yield self
|
25
|
+
finish
|
26
|
+
end
|
27
|
+
|
28
|
+
def remote_list
|
29
|
+
remote = call build_request('/list')
|
30
|
+
JSON.parse(remote.body)['files'].map do |stat|
|
31
|
+
stat['updated_at'] = Time.parse(stat['updated_at'])
|
32
|
+
stat
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def local_list
|
37
|
+
Dir.chdir(@public_folder) do
|
38
|
+
Dir['**/*'].map { |file| neocities_stat(file) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def info
|
43
|
+
info = call build_request('/info')
|
44
|
+
JSON.parse(info.body)['info']
|
45
|
+
end
|
46
|
+
|
47
|
+
def finish
|
48
|
+
@connection.finish if @connection.started?
|
49
|
+
end
|
50
|
+
|
51
|
+
def pull(test: false)
|
52
|
+
file_list = remote_list
|
53
|
+
finish
|
54
|
+
orphans = select_orphans(file_list, local_list) do |path|
|
55
|
+
warn "deleting #{path}" if @verbose
|
56
|
+
|
57
|
+
"#{@public_folder}/#{path}"
|
58
|
+
end
|
59
|
+
File.unlink(*orphans) unless test
|
60
|
+
download_all file_list, test: test
|
61
|
+
nil # Mute this method
|
62
|
+
end
|
63
|
+
|
64
|
+
def push(test: false)
|
65
|
+
file_list = local_list
|
66
|
+
remove_remote_orphans file_list, test: test
|
67
|
+
upload_all file_list, test: test
|
68
|
+
finish
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def neocities_stat(file)
|
74
|
+
stat = File.stat(file)
|
75
|
+
data = {
|
76
|
+
'path' => file,
|
77
|
+
'is_directory' => stat.directory?,
|
78
|
+
'updated_at' => stat.mtime.round.utc
|
79
|
+
}
|
80
|
+
return data if data['is_directory']
|
81
|
+
|
82
|
+
data['size'] = stat.size
|
83
|
+
data['sha1_hash'] = Digest::SHA1.hexdigest File.read(file)
|
84
|
+
data
|
85
|
+
end
|
86
|
+
|
87
|
+
def select_orphans(to_apply, current_list, &block)
|
88
|
+
paths_to_apply = to_apply.map { _1['path'] }
|
89
|
+
current_paths = current_list.map { _1['path'] }
|
90
|
+
(current_paths - paths_to_apply).filter_map(&block)
|
91
|
+
end
|
92
|
+
|
93
|
+
def remove_remote_orphans(file_list, test: false)
|
94
|
+
request = build_request '/delete', :post
|
95
|
+
orphan_paths = select_orphans(file_list, remote_list) do |path|
|
96
|
+
# Never remove the following files. If needed you can still
|
97
|
+
# overwrite them. And in any case, neocities will not allow
|
98
|
+
# the index.html file to be removed.
|
99
|
+
next if PROTECTED_FILES.include? path
|
100
|
+
|
101
|
+
warn "deleting #{path}" if @verbose
|
102
|
+
path
|
103
|
+
end
|
104
|
+
request.form_data = { 'filenames[]' => orphan_paths }
|
105
|
+
return if test
|
106
|
+
|
107
|
+
call request
|
108
|
+
end
|
109
|
+
|
110
|
+
def download_all(file_list, test: false)
|
111
|
+
publish_domain = "#{@website_name}.#{@endpoint.host}"
|
112
|
+
Dir.chdir(@public_folder) do
|
113
|
+
Net::HTTP.start(publish_domain, use_ssl: true) do |http|
|
114
|
+
file_list.each do |file_data|
|
115
|
+
path = file_data['path']
|
116
|
+
file_data['uri'] = "https://#{publish_domain}/#{path}"
|
117
|
+
download_file http, file_data, test: test
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def download_file(http, file_data, test: false)
|
124
|
+
path = file_data['path']
|
125
|
+
if file_data['is_directory']
|
126
|
+
warn "#{path}/" if @verbose
|
127
|
+
FileUtils.mkdir_p path unless test
|
128
|
+
return
|
129
|
+
end
|
130
|
+
|
131
|
+
warn path if @verbose
|
132
|
+
|
133
|
+
content = fetch_file_content(
|
134
|
+
http, file_data['uri'], file_data['sha1_hash']
|
135
|
+
)
|
136
|
+
return unless content && !test
|
137
|
+
|
138
|
+
save_file path, content, file_data['updated_at']
|
139
|
+
end
|
140
|
+
|
141
|
+
def fetch_file_content(http, uri, sha1sum)
|
142
|
+
# Neocities redirect HTML file to location without extension and
|
143
|
+
# redirect index.html file to /
|
144
|
+
uri = uri.delete_suffix('index.html').delete_suffix('.html')
|
145
|
+
content = http.get(uri).body
|
146
|
+
check = Digest::SHA1.hexdigest content
|
147
|
+
return content if check == sha1sum
|
148
|
+
|
149
|
+
warn "SHA1 hash differ for #{uri}"
|
150
|
+
end
|
151
|
+
|
152
|
+
def save_file(path, content, updated_at)
|
153
|
+
File.write path, content
|
154
|
+
FileUtils.touch path, mtime: updated_at
|
155
|
+
path
|
156
|
+
end
|
157
|
+
|
158
|
+
def prepare_files_to_upload(file_list)
|
159
|
+
Dir.chdir(@public_folder) do
|
160
|
+
file_list.filter_map do |file_data|
|
161
|
+
# No need to push intermediary directories, they are created
|
162
|
+
# on the fly
|
163
|
+
next if file_data['is_directory']
|
164
|
+
|
165
|
+
path = file_data['path']
|
166
|
+
warn path if @verbose
|
167
|
+
[path, File.new(path)]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def upload_all(file_list, test: false)
|
173
|
+
form_data = prepare_files_to_upload file_list
|
174
|
+
return if test
|
175
|
+
|
176
|
+
request = build_request '/upload', :post
|
177
|
+
request.set_form form_data, 'multipart/form-data'
|
178
|
+
call request
|
179
|
+
end
|
180
|
+
|
181
|
+
def extract_connection_details(connection_spec)
|
182
|
+
# Do not put your password into the fronde config. The password
|
183
|
+
# is expectfed to be found in a specific config file (not to be
|
184
|
+
# shared).
|
185
|
+
@website_name, endpoint = connection_spec.split('@', 2)
|
186
|
+
endpoint ||= 'neocities.org'
|
187
|
+
@endpoint = URI("https://#{endpoint}/api")
|
188
|
+
# Will raise Errno::ENOENT if file does not exist
|
189
|
+
credentials = YAML.load_file('.credentials')
|
190
|
+
# Will raise KeyError if not set
|
191
|
+
password = credentials.fetch("#{@website_name}_neocities_pass")
|
192
|
+
authorization = [[@website_name, password].join(':')].pack('m0')
|
193
|
+
@authorization = "Basic #{authorization}"
|
194
|
+
end
|
195
|
+
|
196
|
+
def build_request(path, method = :get)
|
197
|
+
uri = @endpoint.dup
|
198
|
+
uri.path += path
|
199
|
+
klass = Kernel.const_get "Net::HTTP::#{method.to_s.capitalize}"
|
200
|
+
klass.new uri
|
201
|
+
end
|
202
|
+
|
203
|
+
def call(request)
|
204
|
+
request['Authorization'] = @authorization
|
205
|
+
@connection.start unless @connection.started?
|
206
|
+
outcome = @connection.request request
|
207
|
+
return outcome if outcome.is_a? Net::HTTPSuccess
|
208
|
+
|
209
|
+
raise JSON.parse(outcome.body).inspect
|
210
|
+
end
|
211
|
+
|
212
|
+
def init_connection
|
213
|
+
http = Net::HTTP.new @endpoint.host, 443
|
214
|
+
http.use_ssl = true
|
215
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
216
|
+
http
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../config'
|
4
|
+
|
5
|
+
module Fronde
|
6
|
+
module Sync
|
7
|
+
# Everything needed to push or pull data with rsync
|
8
|
+
class Rsync
|
9
|
+
def initialize(remote_path, local_path, verbose: false)
|
10
|
+
@verbose = verbose
|
11
|
+
@remote_path = remote_path
|
12
|
+
@local_path = "#{local_path}/"
|
13
|
+
end
|
14
|
+
|
15
|
+
def pull(test: false)
|
16
|
+
run command(test: test) + [@remote_path, @local_path]
|
17
|
+
end
|
18
|
+
|
19
|
+
def push(test: false)
|
20
|
+
run command(test: test) + [@local_path, @remote_path]
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def run(cmd)
|
26
|
+
warn cmd.join(' ') if @verbose
|
27
|
+
# Be precise about Kernel to allow mock in rspec
|
28
|
+
Kernel.system(*cmd)
|
29
|
+
end
|
30
|
+
|
31
|
+
def command(test: false)
|
32
|
+
rsync_command = Fronde::CONFIG.get('rsync')
|
33
|
+
return rsync_command unless rsync_command.nil?
|
34
|
+
|
35
|
+
optstring = []
|
36
|
+
optstring << 'n' if test
|
37
|
+
if @verbose
|
38
|
+
optstring << 'v'
|
39
|
+
else
|
40
|
+
optstring << 'q'
|
41
|
+
end
|
42
|
+
['rsync', "-#{optstring.join}rlt", '--delete']
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/fronde/sync.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'config'
|
4
|
+
require_relative 'sync/rsync'
|
5
|
+
require_relative 'sync/neocities'
|
6
|
+
|
7
|
+
module Fronde
|
8
|
+
# Entrypoint for synchronization with remote public server
|
9
|
+
module Sync
|
10
|
+
class Error < ::StandardError; end
|
11
|
+
|
12
|
+
ALLOWED_SYNCER = %w[rsync neocities].freeze
|
13
|
+
|
14
|
+
def self.extract_method_and_remote(type)
|
15
|
+
remote_path = Fronde::CONFIG.get("#{type}_remote")
|
16
|
+
raise Error, "No #{type} remote path set" if remote_path.nil?
|
17
|
+
|
18
|
+
method, remote = remote_path.split(':', 2)
|
19
|
+
return [method, remote] if ALLOWED_SYNCER.include?(method)
|
20
|
+
|
21
|
+
['rsync', remote_path]
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.pull_or_push(direction, type, test: false, verbose: false)
|
25
|
+
method, remote_path = extract_method_and_remote type
|
26
|
+
public_folder = Fronde::CONFIG.get("#{type}_public_folder")
|
27
|
+
klass = Kernel.const_get("::Fronde::Sync::#{method.capitalize}")
|
28
|
+
syncer = klass.new(remote_path, public_folder, verbose: verbose)
|
29
|
+
Thread.new { syncer.send(direction, test: test) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|