fronde 0.4.0 → 0.6.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ext/nil_time.rb +3 -6
  3. data/lib/ext/time.rb +10 -17
  4. data/lib/ext/time_no_time.rb +27 -0
  5. data/lib/fronde/cli/commands.rb +18 -14
  6. data/lib/fronde/cli/data/fish_completion +20 -0
  7. data/lib/fronde/cli/data/gitignore +0 -1
  8. data/lib/fronde/cli/helpers.rb +0 -2
  9. data/lib/fronde/cli/opt_parse.rb +15 -18
  10. data/lib/fronde/cli/throbber.rb +35 -18
  11. data/lib/fronde/cli.rb +4 -3
  12. data/lib/fronde/config/data/org-config.el +3 -2
  13. data/lib/fronde/config/data/ox-fronde.el +91 -46
  14. data/lib/fronde/config/data/themes/umaneti/css/htmlize.css +364 -0
  15. data/lib/fronde/config/data/themes/umaneti/css/style.css +250 -0
  16. data/lib/fronde/config/data/themes/umaneti/img/bottom.png +0 -0
  17. data/lib/fronde/config/data/themes/umaneti/img/content.png +0 -0
  18. data/lib/fronde/config/data/themes/umaneti/img/tic.png +0 -0
  19. data/lib/fronde/config/data/themes/umaneti/img/top.png +0 -0
  20. data/lib/fronde/config/helpers.rb +1 -19
  21. data/lib/fronde/config/lisp.rb +14 -7
  22. data/lib/fronde/config.rb +47 -31
  23. data/lib/fronde/emacs.rb +23 -9
  24. data/lib/fronde/index/atom_generator.rb +1 -1
  25. data/lib/fronde/index/data/all_tags.org +6 -1
  26. data/lib/fronde/index/data/template.org +8 -4
  27. data/lib/fronde/index/org_generator.rb +10 -6
  28. data/lib/fronde/index.rb +19 -17
  29. data/lib/fronde/org/file.rb +71 -39
  30. data/lib/fronde/org/file_extracter.rb +23 -12
  31. data/lib/fronde/org.rb +14 -12
  32. data/lib/fronde/slug.rb +39 -12
  33. data/lib/fronde/source/gemini.rb +4 -9
  34. data/lib/fronde/source/html.rb +9 -9
  35. data/lib/fronde/source.rb +17 -12
  36. data/lib/fronde/sync/neocities.rb +220 -0
  37. data/lib/fronde/sync/rsync.rb +46 -0
  38. data/lib/fronde/sync.rb +32 -0
  39. data/lib/fronde/templater.rb +35 -51
  40. data/lib/fronde/version.rb +1 -1
  41. data/lib/tasks/cli.rake +45 -13
  42. data/lib/tasks/org.rake +30 -35
  43. data/lib/tasks/site.rake +63 -41
  44. data/lib/tasks/sync.rake +19 -50
  45. data/lib/tasks/tags.rake +2 -2
  46. data/locales/en.yml +143 -81
  47. data/locales/fr.yml +153 -89
  48. metadata +56 -17
  49. data/lib/ext/r18n.rb +0 -17
data/lib/fronde/org.rb CHANGED
@@ -10,7 +10,7 @@ module Fronde
10
10
  class << self
11
11
  def current_version
12
12
  # Do not crash if Org is not yet installed (and thus return nil)
13
- Dir['lib/org-*'].first&.delete_prefix('lib/org-')
13
+ Dir.glob('lib/org-*').first&.delete_prefix('lib/org-')
14
14
  end
15
15
 
16
16
  # Fetch and return the last published version of Org.
@@ -55,23 +55,25 @@ module Fronde
55
55
  # @param destination [String] where to save the org-mode tarball
56
56
  # @return [String] the downloaded org-mode version
57
57
  def download(destination = 'var/tmp')
58
- # Remove version number in dest file to allow easy rake file
59
- # task naming
60
- dest_file = ::File.expand_path('org.tar.gz', destination)
61
58
  org_last_version = last_version(force: false, cookie_dir: destination)
62
59
  tarball = "org-mode-release_#{org_last_version}.tar.gz"
63
60
  uri = URI("https://git.savannah.gnu.org/cgit/emacs/org-mode.git/snapshot/#{tarball}")
64
61
  # Will crash on purpose if anything goes wrong
65
62
  Net::HTTP.start(uri.host) do |http|
66
- request = Net::HTTP::Get.new uri
63
+ fetch_org_tarball http, Net::HTTP::Get.new(uri), destination
64
+ end
65
+ org_last_version
66
+ end
67
67
 
68
- http.request request do |response|
69
- ::File.open(dest_file, 'w') do |io|
70
- response.read_body { |chunk| io.write chunk }
71
- end
68
+ def fetch_org_tarball(http, request, destination)
69
+ # Remove version number in dest file to allow easy rake file
70
+ # task naming
71
+ dest_file = ::File.expand_path('org.tar.gz', destination)
72
+ http.request request do |response|
73
+ ::File.open(dest_file, 'w') do |io|
74
+ response.read_body { |chunk| io.write chunk }
72
75
  end
73
76
  end
74
- org_last_version
75
77
  end
76
78
 
77
79
  def make_org_cmd(org_dir, target, verbose: false)
@@ -95,8 +97,8 @@ module Fronde
95
97
  FileUtils.mv "org-mode-release_#{version}", target
96
98
  # Fix a weird unknown package version
97
99
  ::File.write("#{target}/mk/version.mk", "ORGVERSION ?= #{version}")
98
- system(*make_org_cmd(target, 'compile', verbose: verbose))
99
- system(*make_org_cmd(target, 'autoloads', verbose: verbose))
100
+ system(*make_org_cmd(target, 'compile', verbose:))
101
+ system(*make_org_cmd(target, 'autoloads', verbose:))
100
102
  end
101
103
  end
102
104
  end
data/lib/fronde/slug.rb CHANGED
@@ -5,23 +5,50 @@ module Fronde
5
5
  module Slug
6
6
  class << self
7
7
  def slug(title)
8
- title.downcase.tr(' ', '-')
8
+ title.downcase
9
9
  .encode('ascii', fallback: ->(k) { translit(k) })
10
- .gsub(/[^\w-]/, '').delete_suffix('-')
10
+ .encode('utf-8') # Convert back to utf-8 string
11
+ .gsub(/[^\w-]/, '-')
12
+ .squeeze('-')
13
+ .delete_suffix('-')
11
14
  end
12
15
 
16
+ # rubocop:disable Metrics/CyclomaticComplexity
17
+ # rubocop:disable Metrics/MethodLength
13
18
  def translit(char)
14
- return 'a' if %w[á à â ä ǎ ã å].include?(char)
15
- return 'e' if %w[é è ê ë ě ẽ].include?(char)
16
- return 'i' if %w[í ì î ï ǐ ĩ].include?(char)
17
- return 'o' if %w[ó ò ô ö ǒ õ].include?(char)
18
- return 'u' if %w[ú ù û ü ǔ ũ].include?(char)
19
- return 'y' if %w[ý ŷ ÿ ỹ].include?(char)
20
- return 'c' if char == 'ç'
21
- return 'n' if char == 'ñ'
22
-
23
- '-'
19
+ case char
20
+ when 'á', 'à', 'â', 'ä', 'ǎ', 'ã', 'å'
21
+ 'a'
22
+ when 'é', 'è', 'ê', 'ë', 'ě', 'ẽ', '€'
23
+ 'e'
24
+ when 'í', 'ì', 'î', 'ï', 'ǐ', 'ĩ'
25
+ 'i'
26
+ when 'ó', 'ò', 'ô', 'ö', 'ǒ', 'õ', 'ø'
27
+ 'o'
28
+ when 'ú', 'ù', 'û', 'ü', 'ǔ', 'ũ'
29
+ 'u'
30
+ when 'ý', 'ỳ', 'ŷ', 'ÿ', 'ỹ'
31
+ 'y'
32
+ when 'ç', '©', '🄯'
33
+ 'c'
34
+ when 'ñ'
35
+ 'n'
36
+ when 'ß'
37
+ 'ss'
38
+ when 'œ'
39
+ 'oe'
40
+ when 'æ'
41
+ 'ae'
42
+ when '®'
43
+ 'r'
44
+ when '™'
45
+ 'tm'
46
+ else
47
+ '-'
48
+ end
24
49
  end
50
+ # rubocop:enable Metrics/CyclomaticComplexity
51
+ # rubocop:enable Metrics/MethodLength
25
52
  end
26
53
  end
27
54
  end
@@ -4,18 +4,13 @@ module Fronde
4
4
  class Source
5
5
  # Specific settings for Gemini {Fronde::Source}
6
6
  class Gemini < Source
7
- def blog?
8
- # TODO: See how to support blog/indexes with gemini
9
- false
10
- end
11
-
12
7
  class << self
13
8
  def org_default_postamble
14
9
  format(
15
10
  "📅 %<date>s\n📝 %<author>s %<creator>s",
16
- author: R18n.t.fronde.org.postamble.written_by,
17
- creator: R18n.t.fronde.org.postamble.with_emacs,
18
- date: R18n.t.fronde.org.postamble.last_modification
11
+ author: I18n.t('fronde.org.postamble.written_by'),
12
+ creator: I18n.t('fronde.org.postamble.with_emacs'),
13
+ date: I18n.t('fronde.org.postamble.last_modification')
19
14
  )
20
15
  end
21
16
  end
@@ -25,7 +20,7 @@ module Fronde
25
20
  def fill_in_specific_config
26
21
  @config.merge!(
27
22
  'type' => 'gemini', 'ext' => '.gmi', 'mime_type' => 'text/gemini',
28
- 'folder' => CONFIG.get('gemini_public_folder')
23
+ 'folder' => File.expand_path(CONFIG.get('gemini_public_folder'))
29
24
  )
30
25
  end
31
26
 
@@ -13,9 +13,9 @@ module Fronde
13
13
  class << self
14
14
  def org_default_postamble
15
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>
16
+ <p><span class="author">#{I18n.t('fronde.org.postamble.written_by')}</span>
17
+ #{I18n.t('fronde.org.postamble.with_emacs_html')}</p>
18
+ <p class="date">#{I18n.t('fronde.org.postamble.last_modification')}</p>
19
19
  <p class="validation">%v</p>
20
20
  POSTAMBLE
21
21
  end
@@ -26,7 +26,7 @@ module Fronde
26
26
  def fill_in_specific_config
27
27
  @config.merge!(
28
28
  'type' => 'html', 'ext' => '.html', 'mime_type' => 'text/html',
29
- 'folder' => CONFIG.get('html_public_folder')
29
+ 'folder' => File.expand_path(CONFIG.get('html_public_folder'))
30
30
  )
31
31
  end
32
32
 
@@ -40,12 +40,12 @@ module Fronde
40
40
  super
41
41
  end
42
42
 
43
- def org_default_options # rubocop:disable Metrics/MethodLength
43
+ def org_default_options
44
44
  defaults = {
45
45
  'publishing-function' => 'org-html-publish-to-html',
46
46
  'html-head-include-default-style' => 't',
47
47
  'html-head-include-scripts' => 't',
48
- 'html-head' => '{{ atom_feed }}',
48
+ 'html-head' => '%F',
49
49
  'html-postamble' => Html.org_default_postamble
50
50
  }
51
51
  return defaults if @config['theme'] == 'default'
@@ -55,10 +55,10 @@ module Fronde
55
55
  'html-head-include-scripts' => 'nil',
56
56
  'html-head' => <<~HTMLHEAD
57
57
  <link rel="stylesheet" type="text/css" media="screen"
58
- href="{{ domain }}/assets/{{ theme }}/css/style.css">
58
+ href="%h/assets/%o/css/style.css">
59
59
  <link rel="stylesheet" type="text/css" media="screen"
60
- href="{{ domain }}/assets/{{ theme }}/css/htmlize.css">
61
- {{ atom_feed }}
60
+ href="%h/assets/%o/css/htmlize.css">
61
+ %F
62
62
  HTMLHEAD
63
63
  )
64
64
  end
data/lib/fronde/source.rb CHANGED
@@ -58,11 +58,11 @@ module Fronde
58
58
  def source_for(file_name)
59
59
  relative_file_path = file_name.delete_prefix "#{publication_path}/"
60
60
  # file_name does not begin with source path.
61
- return nil if relative_file_path == file_name
61
+ return if relative_file_path == file_name
62
62
 
63
63
  # Looks like a file at a deeper level, but current source is not
64
64
  # recursive.
65
- return nil if relative_file_path.include?('/') && !recursive?
65
+ return if relative_file_path.include?('/') && !recursive?
66
66
 
67
67
  # Looks like a match. But does a source file for this one actually
68
68
  # exists?
@@ -70,7 +70,7 @@ module Fronde
70
70
  /#{@config['ext']}\z/, '.org'
71
71
  )
72
72
  source_path = File.join(@config['path'], relative_source_path)
73
- return nil unless File.file?(source_path)
73
+ return unless File.file?(source_path)
74
74
 
75
75
  source_path
76
76
  end
@@ -111,7 +111,7 @@ module Fronde
111
111
  def publication_path
112
112
  return @config['publication_path'] if @config['publication_path']
113
113
 
114
- publish_in = [File.expand_path(@config['folder']), @config['target']]
114
+ publish_in = [@config['folder'], @config['target']]
115
115
  @config['publication_path'] = publish_in.join('/').delete_suffix('/')
116
116
  end
117
117
 
@@ -147,7 +147,7 @@ module Fronde
147
147
 
148
148
  def clean_config
149
149
  fill_in_specific_config
150
- @config['name'] ||= @config['path'].sub(/^[.~]*\//, '').tr('/.', '-')
150
+ @config['name'] ||= @config['path'].sub(%r{^[.~]*/}, '').tr('/.', '-')
151
151
  @config['title'] ||= @config['path']
152
152
  @config['target'] ||= File.basename(@config['path']).delete_prefix '.'
153
153
  @config['target'] = '' if @config['target'] == '.'
@@ -168,12 +168,16 @@ module Fronde
168
168
  end
169
169
 
170
170
  def render_heading
171
- heading_key = "#{@config['type']}-head"
172
- heading = @config.dig 'org-options', heading_key
173
- @config['org-options'][heading_key] = \
174
- Config::Helpers.render_liquid_template(
175
- heading, to_h
176
- )
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
177
181
  end
178
182
 
179
183
  def org_project_config
@@ -181,7 +185,8 @@ module Fronde
181
185
  'base-directory' => @config['path'],
182
186
  'base-extension' => 'org',
183
187
  'publishing-directory' => publication_path,
184
- 'recursive' => @config['recursive']
188
+ 'recursive' => @config['recursive'],
189
+ 'fronde-base-uri' => "#{@config['domain']}#{public_absolute_path}"
185
190
  }.merge(@config['org-options'])
186
191
  exclude = @config['exclude']
187
192
  attributes['exclude'] = exclude if exclude
@@ -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.glob('**/*').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
+ puts I18n.t('fronde.neocities.deleting', path:) if @verbose
56
+
57
+ "#{@public_folder}/#{path}"
58
+ end
59
+ File.unlink(*orphans) unless test
60
+ download_all(file_list, 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:)
67
+ upload_all(file_list, 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, &)
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(&)
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
+ puts I18n.t('fronde.neocities.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:
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
+ puts "#{path}/" if @verbose
127
+ FileUtils.mkdir_p path unless test
128
+ return
129
+ end
130
+
131
+ puts 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 I18n.t('fronde.neocities.sha1_differ', 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
+ puts 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:) + [@remote_path, @local_path]
17
+ end
18
+
19
+ def push(test: false)
20
+ run command(test:) + [@local_path, @remote_path]
21
+ end
22
+
23
+ private
24
+
25
+ def run(cmd)
26
+ puts 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
@@ -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:)
29
+ Thread.new { syncer.send(direction, test:) }
30
+ end
31
+ end
32
+ end