Class: NeocitiesRed::CLI

Inherits:
Object
  • Object
show all
Defined in:
lib/neocities_red/cli.rb

Constant Summary collapse

SUBCOMMANDS =
%w[upload delete list info push logout pizza pull purge diff].freeze
HELP_SUBCOMMANDS =
["-h", "--help", "help"].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(argv) ⇒ CLI

Returns a new instance of CLI.



22
23
24
25
26
27
28
29
30
# File 'lib/neocities_red/cli.rb', line 22

def initialize(argv)
  @argv = argv.dup
  @display = NeocitiesRed::CliDisplay.new
  @subcmd = @argv.first
  @subargs = @argv[1..@argv.length]
  @prompt = TTY::Prompt.new
  @api_key = ENV["NEOCITIES_API_KEY"] || nil
  @app_config_path = File.join self.class.app_config_path("neocities"), "config.json"
end

Class Method Details

.app_config_path(name) ⇒ Object



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/neocities_red/cli.rb', line 386

def self.app_config_path(name)
  platform = case RUBY_PLATFORM
             when /cygwin|mswin|mingw|bccwin|wince|emx|win32/
               :windows
             when /darwin/
               :darwin
             when /linux/
               :linux
             when /freebsd/
               :freebsd
             else
               :unknown
             end

  case platform
  when :linux
    return File.join(ENV["XDG_CONFIG_HOME"], name) if ENV["XDG_CONFIG_HOME"]
    return File.join(Dir.home, ".config", name) if Dir.home
  when :darwin
    return File.join(Dir.home, "Library", "Application Support", name) if Dir.home
  when :windows
    return File.join(ENV["LOCALAPPDATA"], name) if ENV["LOCALAPPDATA"]

    if ENV["USERPROFILE"]
      return File.join(
        ENV["USERPROFILE"],
        "AppData",
        "Local",
        name
      )
    end
  else
    # FreeBSD and other unknown UNIX-like systems use dotfile directly in home directory
    return File.join(Dir.home, ".#{name}") if Dir.home
  end

  nil
end

Instance Method Details

#deleteObject



148
149
150
151
152
153
154
# File 'lib/neocities_red/cli.rb', line 148

def delete
  @display.display_delete_help_and_exit if @subargs.empty?

  @subargs.each do |path|
    Services::FileRemover.new(@client, path).remove
  end
end

#diffObject



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/neocities_red/cli.rb', line 94

def diff
  @display.display_diff_help_and_exit if @subargs.empty?

  @ignore_dotfiles = false
  @path = "."
  @exclude = []

  loop do
    arg = @subargs[0]
    break if arg.nil?

    if arg == "--ignore-dotfiles"
      @subargs.shift
      @ignore_dotfiles = true

    elsif arg == "-e"
      @subargs.shift

      base = Pathname.new(@path).expand_path
      target = Pathname.new(@subargs[0]).expand_path
      filepath = target.relative_path_from(base).to_s

      if File.file?(target)
        @exclude << filepath
      elsif File.directory?(target)
        @exclude += Dir.glob(
          File.join(target, "**", "*"),
          File::FNM_DOTMATCH
        ).map do |path|
          Pathname.new(path).expand_path.relative_path_from(base).to_s
        end

        @exclude << filepath
      end

      @subargs.shift

    elsif File.directory?(arg)
      @path = arg
      @subargs.shift
    end
  end

  added, modified, removed = Services::SiteDifference.new(
    @client,
    path: @path,
    detail: false,
    ignore_dotfiles: @ignore_dotfiles,
    exclude: @exclude
  ).show

  @display.display_diff_results(added: added, modified: modified, removed: removed)
end

#infoObject



180
181
182
183
184
185
# File 'lib/neocities_red/cli.rb', line 180

def info
  profile_info = Services::ProfileInfo.new(@client, @subargs, @sitename).pretty_print
  @display.say TTY::Table.new(profile_info)
rescue StandardError => e
  @display.display_response(e)
end

#listObject



187
188
189
190
191
192
193
194
195
196
197
# File 'lib/neocities_red/cli.rb', line 187

def list
  @display.display_list_help_and_exit if @subargs.empty?

  @detail = true if @subargs.delete("-d") == "-d"

  @subargs[0] = nil if @subargs.delete("-a")

  path = @subargs[0]

  @display.say Services::FileList.new(@client, path, @detail).show
end

#logoutObject



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/neocities_red/cli.rb', line 156

def logout
  confirmed = false

  loop do
    case @subargs[0]
    when "-y"
      @subargs.shift
      confirmed = true
    when /^-/
      @display.display_unknown_option(@subargs[0])
      break
    else
      break
    end
  end

  if confirmed
    FileUtils.rm @app_config_path
    @display.display_logout_success
  else
    @display.display_logout_help_and_exit
  end
end

#pizzaObject



382
383
384
# File 'lib/neocities_red/cli.rb', line 382

def pizza
  @display.display_pizza_help_and_exit
end

#pullObject



354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/neocities_red/cli.rb', line 354

def pull
  quiet = ["--quiet", "-q"].include?(@subargs[0])

  file = File.read(@app_config_path)
  data = JSON.parse(file)

  last_pull_time = data.dig("LAST_PULL", "time")
  last_pull_loc = data.dig("LAST_PULL", "loc")

  Services::SiteExporter.new(@client, @sitename, data, @app_config_path)
                        .export(quiet:, last_pull_time:, last_pull_loc:)
end

#purgeObject

only for development purposes



368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/neocities_red/cli.rb', line 368

def purge
  resp = @client.list
  resp[:files].each do |file|
    @display.display_delete_progress(file[:path])
    resp = @client.delete_wrapper_with_dry_run file[:path], @dry_run

    if resp[:result] == "success"
      @display.display_delete_success
    else
      @display.display_delete_error(resp)
    end
  end
end

#pushObject



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/neocities_red/cli.rb', line 199

def push
  @display.display_push_help_and_exit if @subargs.empty?
  @no_gitignore = false
  @ignore_dotfiles = false
  @excluded_files = []
  @dry_run = false
  @prune = false
  @optimized = false

  loop do
    case @subargs[0]
    when "--no-gitignore"
      @subargs.shift
      @no_gitignore = true
    when "--ignore-dotfiles"
      @subargs.shift
      @ignore_dotfiles = true
    when "-e"
      @subargs.shift
      filepath = Pathname.new(@subargs.shift).cleanpath.to_s

      if File.file?(filepath)
        @excluded_files.push(filepath)
      elsif File.directory?(filepath)
        folder_files = Dir.glob(File.join(filepath, "**", "*"), File::FNM_DOTMATCH).push(filepath)
        @excluded_files += folder_files
      end
    when "--dry-run"
      @subargs.shift
      @dry_run = true
    when "--prune"
      @subargs.shift
      @prune = true
    when "--optimized"
      @subargs.shift
      @optimized = true
    when /^-/
      @display.display_unknown_option(@subargs[0])
      @display.display_push_help_and_exit
    else
      break
    end
  end

  if @subargs[0].nil?
    @display.display_response(result: "error", message: "no local path provided")
    @display.display_push_help_and_exit
  end

  root_path = Pathname @subargs[0]

  unless root_path.exist?
    @display.display_response(result: "error", message: "path #{root_path} does not exist")
    @display.display_push_help_and_exit
  end

  unless root_path.directory?
    @display.display_response(result: "error", message: "provided path is not a directory")
    @display.display_push_help_and_exit
  end

  @display.display_dry_run_notice if @dry_run

  if @prune
    pruned_dirs = []
    resp = @client.list
    resp[:files].each do |file|
      path = Pathname(File.join(@subargs[0], file[:path]))

      pruned_dirs << path if !path.exist? && file[:is_directory]

      next unless !path.exist? && !pruned_dirs.include?(path.dirname)

      @display.display_delete_progress(file[:path])
      resp = @client.delete_wrapper_with_dry_run file[:path], @dry_run

      if resp[:result] == "success"
        @display.display_delete_success
      else
        @display.display_delete_error(resp)
      end
    end
  end

  Dir.chdir(root_path) do
    paths = Dir.glob(File.join("**", "*"), File::FNM_DOTMATCH)

    if @no_gitignore == false && File.exist?(".gitignore")
      ignores = File.readlines(".gitignore").map do |ignore|
        ignore = ignore.strip
        File.directory?(ignore) ? "#{ignore}**" : ignore
      end

      paths.select! do |path|
        ignores.none? { |ignore| File.fnmatch?(ignore, path) }
      end

      @display.display_gitignore_hint
    end

    @excluded_files += paths.select { |path| path.start_with?(".") } if @ignore_dotfiles

    # do not upload files which already uploaded (checking by sha1_hash)
    if @optimized
      hex = paths.select { |path| File.file?(path) }
                 .map { |file| { filepath: file, sha1_hash: Digest::SHA1.file(file).hexdigest } }

      res = @client.list
      server_hex = res[:files].map { |n| n[:sha1_hash] }.compact

      uploaded_files = hex.select { |n| server_hex.include?(n[:sha1_hash]) }
                          .map { |n| n[:filepath] }
      @excluded_files += uploaded_files
    end

    paths -= @excluded_files
    paths.collect! { |path| Pathname path }

    task_queue = Queue.new
    paths.each { |path| task_queue.push(path) }

    threads = []

    MAX_THREADS.times do
      threads << Thread.new do
        until task_queue.empty?
          path = begin
            task_queue.pop(true)
          rescue StandardError
            nil
          end
          next if path.nil? || path.directory?

          Services::FileUploader.new(@client, path, path).upload
        end
      end
    end

    threads.each(&:join)
    @display.display_upload_complete
  end
end

#runObject



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/neocities_red/cli.rb', line 32

def run
  if @argv[0] == "version"
    @display.say NeocitiesRed::VERSION
    exit
  end

  if HELP_SUBCOMMANDS.include?(@subcmd) && SUBCOMMANDS.include?(@subargs[0])
    @display.public_send("display_#{@subargs[0]}_help_and_exit")
  elsif @subcmd.nil? || !SUBCOMMANDS.include?(@subcmd)
    @display.display_help_and_exit
  elsif @subargs.join.match(HELP_SUBCOMMANDS.join("|")) && @subcmd != "info"
    @display.public_send("display_#{@subcmd}_help_and_exit")

  end

  unless @api_key
    begin
      file = File.read @app_config_path
      data = JSON.parse file

      if data
        @api_key = data["API_KEY"].strip
        @sitename = data["SITENAME"]
        @last_pull = data["LAST_PULL"] # Store the last time a pull was performed so that we only fetch from updated files
      end
    rescue Errno::ENOENT
      @api_key = nil
    end
  end

  if @api_key.nil?
    @display.

    if !@sitename && !@password
      @sitename = @prompt.ask("sitename:", default: ENV.fetch("NEOCITIES_SITENAME", nil))
      @password = @prompt.mask("password:", default: ENV.fetch("NEOCITIES_PASSWORD", nil))
    end

    @client = NeocitiesRed::Client.new sitename: @sitename, password: @password

    resp = @client.key
    if resp[:api_key]
      conf = {
        API_KEY: resp[:api_key],
        SITENAME: @sitename
      }

      FileUtils.mkdir_p Pathname(@app_config_path).dirname
      File.write @app_config_path, conf.to_json

      @display.display_api_key_saved(@sitename, @app_config_path)
    else
      @display.display_response(resp)
      exit
    end
  else
    @client = NeocitiesRed::Client.new api_key: @api_key
  end

  send @subcmd
end

#uploadObject



342
343
344
345
346
347
348
349
350
351
352
# File 'lib/neocities_red/cli.rb', line 342

def upload
  @display.display_upload_help_and_exit if @subargs[0].nil? || @subargs[1].nil?

  if File.file?(@subargs[0])
    Services::FileUploader.new(@client, @subargs[0], @subargs[1]).upload
  elsif File.directory?(@subargs[0])
    folder_uploader = Services::FolderUploader.new(@client, @subargs[0], @subargs[1])
    files_list = folder_uploader.files
    folder_uploader.upload(files_list)
  end
end