neocities 0.0.1 → 0.0.20
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 +5 -5
- data/.gitignore +2 -16
- data/Gemfile +0 -1
- data/README.md +14 -16
- data/bin/neocities +4 -0
- data/ext/mkrf_conf.rb +19 -0
- data/lib/neocities/cli.rb +542 -0
- data/lib/neocities/client.rb +183 -0
- data/lib/neocities/version.rb +1 -1
- data/lib/neocities.rb +3 -0
- data/neocities.gemspec +12 -7
- metadata +113 -18
- data/LICENSE.txt +0 -22
- data/Rakefile +0 -1
- data/lib/neocities/neocities.rb +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5031613f2ead49cb8b0d897ba928dca50121afbc65b0f624afd89038c6f97c3e
|
4
|
+
data.tar.gz: 3f6a7813f75c2d0d667700258a5f2a9a60b74176b69cee5c1d26d95892970a91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b1ae46e13488bbdd244da5e1f3eb005002f6a2e9a687c9682fc2055ccaf054c76de1f9386ed578b7462745acf1c53d7fac81957b41b474b2f0422ab5823d3f09
|
7
|
+
data.tar.gz: a72a64b9bc2ea15a3c7c6ad01f8f405802c5476e96b8975081d3e7dbef531543838eb029fcba108c7cc7a96e89725fac8855500c0c8ffd8922235f05a86fb9b9
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,29 +1,27 @@
|
|
1
|
-
# Neocities
|
1
|
+
# The Neocities Gem
|
2
2
|
|
3
|
-
|
3
|
+
A CLI and library for using the Neocities API. Makes it easy to quickly upload, push, delete, and list your Neocities site.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
-
|
7
|
+
### Mac OSX
|
8
8
|
|
9
|
-
|
9
|
+
If you're using OSX (or already have ruby installed):
|
10
10
|
|
11
|
-
|
11
|
+
gem install neocities
|
12
12
|
|
13
|
-
|
13
|
+
### Ubuntu/Debian
|
14
14
|
|
15
|
-
|
15
|
+
sudo apt-get install ruby
|
16
16
|
|
17
|
-
|
17
|
+
### Windows
|
18
18
|
|
19
|
-
|
19
|
+
Install ruby with [RubyInstaller](https://rubyinstaller.org/). Then you can install the gem with the above command.
|
20
20
|
|
21
|
-
|
21
|
+
### Running
|
22
22
|
|
23
|
-
|
23
|
+
After that, you are all set! Run `neocities` in a command line to see the options and get started.
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
-
5. Create new Pull Request
|
25
|
+
## Neocities::Client
|
26
|
+
|
27
|
+
This gem also ships with Neocities::Client, which you can use to write code that interfaces with the Neocities API.
|
data/bin/neocities
ADDED
data/ext/mkrf_conf.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rubygems/command'
|
3
|
+
require 'rubygems/dependency_installer'
|
4
|
+
begin
|
5
|
+
Gem::Command.build_args = ARGV
|
6
|
+
rescue NoMethodError
|
7
|
+
end
|
8
|
+
inst = Gem::DependencyInstaller.new
|
9
|
+
begin
|
10
|
+
if Gem.win_platform?
|
11
|
+
inst.install 'openssl-win-root', '~> 1.1'
|
12
|
+
end
|
13
|
+
rescue
|
14
|
+
exit(1)
|
15
|
+
end
|
16
|
+
|
17
|
+
f = File.open(File.join(File.dirname(__FILE__), "Rakefile"), "w") # create dummy rakefile to indicate success
|
18
|
+
f.write("task :default\n")
|
19
|
+
f.close
|
@@ -0,0 +1,542 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'pastel'
|
3
|
+
require 'tty/table'
|
4
|
+
require 'tty/prompt'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'json' # for reading configs
|
7
|
+
require 'whirly' # for loader spinner
|
8
|
+
|
9
|
+
require File.join(File.dirname(__FILE__), 'client')
|
10
|
+
|
11
|
+
module Neocities
|
12
|
+
class CLI
|
13
|
+
SUBCOMMANDS = %w{upload delete list info push logout pizza pull}
|
14
|
+
HELP_SUBCOMMANDS = ['-h', '--help', 'help']
|
15
|
+
PENELOPE_MOUTHS = %w{^ o ~ - v U}
|
16
|
+
PENELOPE_EYES = %w{o ~ O}
|
17
|
+
|
18
|
+
def initialize(argv)
|
19
|
+
@argv = argv.dup
|
20
|
+
@pastel = Pastel.new eachline: "\n"
|
21
|
+
@subcmd = @argv.first
|
22
|
+
@subargs = @argv[1..@argv.length]
|
23
|
+
@prompt = TTY::Prompt.new
|
24
|
+
@api_key = ENV['NEOCITIES_API_KEY'] || nil
|
25
|
+
@app_config_path = File.join self.class.app_config_path('neocities'), 'config.json' # added json extension
|
26
|
+
end
|
27
|
+
|
28
|
+
def display_response(resp)
|
29
|
+
if resp[:result] == 'success'
|
30
|
+
puts "#{@pastel.green.bold 'SUCCESS:'} #{resp[:message]}"
|
31
|
+
elsif resp[:result] == 'error' && resp[:error_type] == 'file_exists'
|
32
|
+
out = "#{@pastel.yellow.bold 'EXISTS:'} #{resp[:message]}"
|
33
|
+
out += " (#{resp[:error_type]})" if resp[:error_type]
|
34
|
+
puts out
|
35
|
+
else
|
36
|
+
out = "#{@pastel.red.bold 'ERROR:'} #{resp[:message]}"
|
37
|
+
out += " (#{resp[:error_type]})" if resp[:error_type]
|
38
|
+
puts out
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def run
|
43
|
+
if @argv[0] == 'version'
|
44
|
+
puts Neocities::VERSION
|
45
|
+
exit
|
46
|
+
end
|
47
|
+
|
48
|
+
if HELP_SUBCOMMANDS.include?(@subcmd) && SUBCOMMANDS.include?(@subargs[0])
|
49
|
+
send "display_#{@subargs[0]}_help_and_exit"
|
50
|
+
elsif @subcmd.nil? || !SUBCOMMANDS.include?(@subcmd)
|
51
|
+
display_help_and_exit
|
52
|
+
elsif @subargs.join("").match(HELP_SUBCOMMANDS.join('|')) && @subcmd != "info"
|
53
|
+
send "display_#{@subcmd}_help_and_exit"
|
54
|
+
end
|
55
|
+
|
56
|
+
if !@api_key
|
57
|
+
begin
|
58
|
+
file = File.read @app_config_path
|
59
|
+
data = JSON.load file
|
60
|
+
|
61
|
+
if data
|
62
|
+
@api_key = data["API_KEY"].strip # Remove any trailing whitespace causing HTTP requests to fail
|
63
|
+
@sitename = data["SITENAME"] # Store the sitename to be able to reference it later
|
64
|
+
@last_pull = data["LAST_PULL"] # Store the last time a pull was performed so that we only fetch from updated files
|
65
|
+
end
|
66
|
+
rescue Errno::ENOENT
|
67
|
+
@api_key = nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if @api_key.nil?
|
72
|
+
puts "Please login to get your API key:"
|
73
|
+
|
74
|
+
if !@sitename && !@password
|
75
|
+
@sitename = @prompt.ask('sitename:', default: ENV['NEOCITIES_SITENAME'])
|
76
|
+
@password = @prompt.mask('password:', default: ENV['NEOCITIES_PASSWORD'])
|
77
|
+
end
|
78
|
+
|
79
|
+
@client = Neocities::Client.new sitename: @sitename, password: @password
|
80
|
+
|
81
|
+
resp = @client.key
|
82
|
+
if resp[:api_key]
|
83
|
+
conf = {
|
84
|
+
"API_KEY": resp[:api_key],
|
85
|
+
"SITENAME": @sitename,
|
86
|
+
}
|
87
|
+
|
88
|
+
FileUtils.mkdir_p Pathname(@app_config_path).dirname
|
89
|
+
File.write @app_config_path, conf.to_json
|
90
|
+
|
91
|
+
puts "The api key for #{@pastel.bold @sitename} has been stored in #{@pastel.bold @app_config_path}."
|
92
|
+
else
|
93
|
+
display_response resp
|
94
|
+
exit
|
95
|
+
end
|
96
|
+
else
|
97
|
+
@client = Neocities::Client.new api_key: @api_key
|
98
|
+
end
|
99
|
+
|
100
|
+
send @subcmd
|
101
|
+
end
|
102
|
+
|
103
|
+
def delete
|
104
|
+
display_delete_help_and_exit if @subargs.empty?
|
105
|
+
@subargs.each do |file|
|
106
|
+
puts @pastel.bold("Deleting #{file} ...")
|
107
|
+
resp = @client.delete file
|
108
|
+
|
109
|
+
display_response resp
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def logout
|
114
|
+
confirmed = false
|
115
|
+
loop do
|
116
|
+
case @subargs[0]
|
117
|
+
when '-y' then @subargs.shift; confirmed = true
|
118
|
+
when /^-/ then puts(@pastel.red.bold("Unknown option: #{@subargs[0].inspect}")); break
|
119
|
+
else break
|
120
|
+
end
|
121
|
+
end
|
122
|
+
if confirmed
|
123
|
+
FileUtils.rm @app_config_path
|
124
|
+
puts @pastel.bold("Your api key has been removed.")
|
125
|
+
else
|
126
|
+
display_logout_help_and_exit
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def info
|
131
|
+
resp = @client.info(@subargs[0] || @sitename)
|
132
|
+
|
133
|
+
if resp[:result] == 'error'
|
134
|
+
display_response resp
|
135
|
+
exit
|
136
|
+
end
|
137
|
+
|
138
|
+
out = []
|
139
|
+
|
140
|
+
resp[:info].each do |k, v|
|
141
|
+
v = Time.parse(v).localtime if v && (k == :created_at || k == :last_updated)
|
142
|
+
out.push [@pastel.bold(k), v]
|
143
|
+
end
|
144
|
+
|
145
|
+
puts TTY::Table.new(out).to_s
|
146
|
+
exit
|
147
|
+
end
|
148
|
+
|
149
|
+
def list
|
150
|
+
display_list_help_and_exit if @subargs.empty?
|
151
|
+
if @subargs.delete('-d') == '-d'
|
152
|
+
@detail = true
|
153
|
+
end
|
154
|
+
|
155
|
+
if @subargs.delete('-a')
|
156
|
+
@subargs[0] = nil
|
157
|
+
end
|
158
|
+
|
159
|
+
resp = @client.list @subargs[0]
|
160
|
+
|
161
|
+
if resp[:result] == 'error'
|
162
|
+
display_response resp
|
163
|
+
exit
|
164
|
+
end
|
165
|
+
|
166
|
+
if @detail
|
167
|
+
out = [
|
168
|
+
[@pastel.bold('Path'), @pastel.bold('Size'), @pastel.bold('Updated')]
|
169
|
+
]
|
170
|
+
resp[:files].each do |file|
|
171
|
+
out.push([
|
172
|
+
@pastel.send(file[:is_directory] ? :blue : :green).bold(file[:path]),
|
173
|
+
file[:size] || '',
|
174
|
+
Time.parse(file[:updated_at]).localtime
|
175
|
+
])
|
176
|
+
end
|
177
|
+
puts TTY::Table.new(out).to_s
|
178
|
+
exit
|
179
|
+
end
|
180
|
+
|
181
|
+
resp[:files].each do |file|
|
182
|
+
puts @pastel.send(file[:is_directory] ? :blue : :green).bold(file[:path])
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def push
|
187
|
+
display_push_help_and_exit if @subargs.empty?
|
188
|
+
@no_gitignore = false
|
189
|
+
@excluded_files = []
|
190
|
+
@dry_run = false
|
191
|
+
@prune = false
|
192
|
+
loop do
|
193
|
+
case @subargs[0]
|
194
|
+
when '--no-gitignore' then @subargs.shift; @no_gitignore = true
|
195
|
+
when '-e' then @subargs.shift; @excluded_files.push(@subargs.shift)
|
196
|
+
when '--dry-run' then @subargs.shift; @dry_run = true
|
197
|
+
when '--prune' then @subargs.shift; @prune = true
|
198
|
+
when /^-/ then puts(@pastel.red.bold("Unknown option: #{@subargs[0].inspect}")); display_push_help_and_exit
|
199
|
+
else break
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
if @subargs[0].nil?
|
204
|
+
display_response result: 'error', message: "no local path provided"
|
205
|
+
display_push_help_and_exit
|
206
|
+
end
|
207
|
+
|
208
|
+
root_path = Pathname @subargs[0]
|
209
|
+
|
210
|
+
if !root_path.exist?
|
211
|
+
display_response result: 'error', message: "path #{root_path} does not exist"
|
212
|
+
display_push_help_and_exit
|
213
|
+
end
|
214
|
+
|
215
|
+
if !root_path.directory?
|
216
|
+
display_response result: 'error', message: 'provided path is not a directory'
|
217
|
+
display_push_help_and_exit
|
218
|
+
end
|
219
|
+
|
220
|
+
if @dry_run
|
221
|
+
puts @pastel.green.bold("Doing a dry run, not actually pushing anything")
|
222
|
+
end
|
223
|
+
|
224
|
+
if @prune
|
225
|
+
pruned_dirs = []
|
226
|
+
resp = @client.list
|
227
|
+
resp[:files].each do |file|
|
228
|
+
path = Pathname(File.join(@subargs[0], file[:path]))
|
229
|
+
|
230
|
+
pruned_dirs << path if !path.exist? && (file[:is_directory])
|
231
|
+
|
232
|
+
if !path.exist? && !pruned_dirs.include?(path.dirname)
|
233
|
+
print @pastel.bold("Deleting #{file[:path]} ... ")
|
234
|
+
resp = @client.delete_wrapper_with_dry_run file[:path], @dry_run
|
235
|
+
|
236
|
+
if resp[:result] == 'success'
|
237
|
+
print @pastel.green.bold("SUCCESS") + "\n"
|
238
|
+
else
|
239
|
+
print "\n"
|
240
|
+
display_response resp
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
Dir.chdir(root_path) do
|
247
|
+
paths = Dir.glob(File.join('**', '*'), File::FNM_DOTMATCH)
|
248
|
+
|
249
|
+
if @no_gitignore == false
|
250
|
+
begin
|
251
|
+
ignores = File.readlines('.gitignore').collect! do |ignore|
|
252
|
+
ignore.strip!
|
253
|
+
File.directory?(ignore) ? "#{ignore}**" : ignore
|
254
|
+
end
|
255
|
+
paths.select! do |path|
|
256
|
+
res = true
|
257
|
+
ignores.each do |ignore|
|
258
|
+
if File.fnmatch?(ignore.strip, path)
|
259
|
+
res = false
|
260
|
+
break
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
puts "Not pushing .gitignore entries (--no-gitignore to disable)"
|
265
|
+
rescue Errno::ENOENT
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
paths.select! { |p| !@excluded_files.include?(p) }
|
270
|
+
|
271
|
+
paths.select! { |p| !@excluded_files.include?(Pathname.new(p).dirname.to_s) }
|
272
|
+
|
273
|
+
paths.collect! { |path| Pathname path }
|
274
|
+
|
275
|
+
paths.each do |path|
|
276
|
+
next if path.directory?
|
277
|
+
print @pastel.bold("Uploading #{path} ... ")
|
278
|
+
resp = @client.upload path, path, @dry_run
|
279
|
+
|
280
|
+
if resp[:result] == 'error' && resp[:error_type] == 'file_exists'
|
281
|
+
print @pastel.yellow.bold("EXISTS") + "\n"
|
282
|
+
elsif resp[:result] == 'success'
|
283
|
+
print @pastel.green.bold("SUCCESS") + "\n"
|
284
|
+
else
|
285
|
+
print "\n"
|
286
|
+
display_response resp
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def upload
|
293
|
+
display_upload_help_and_exit if @subargs.empty?
|
294
|
+
@dir = ''
|
295
|
+
|
296
|
+
loop do
|
297
|
+
case @subargs[0]
|
298
|
+
when '-d' then @subargs.shift; @dir = @subargs.shift
|
299
|
+
when /^-/ then puts(@pastel.red.bold("Unknown option: #{@subargs[0].inspect}")); display_upload_help_and_exit
|
300
|
+
else break
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
@subargs.each do |path|
|
305
|
+
path = Pathname path
|
306
|
+
|
307
|
+
if !path.exist?
|
308
|
+
display_response result: 'error', message: "#{path} does not exist locally."
|
309
|
+
next
|
310
|
+
end
|
311
|
+
|
312
|
+
if path.directory?
|
313
|
+
puts "#{path} is a directory, skipping (see the push command)"
|
314
|
+
next
|
315
|
+
end
|
316
|
+
|
317
|
+
remote_path = ['/', @dir, path.basename.to_s].join('/').gsub %r{/+}, '/'
|
318
|
+
|
319
|
+
puts @pastel.bold("Uploading #{path} to #{remote_path} ...")
|
320
|
+
resp = @client.upload path, remote_path
|
321
|
+
display_response resp
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def pull
|
326
|
+
begin
|
327
|
+
quiet = !(['--log', '-l'].include? @subargs[0])
|
328
|
+
|
329
|
+
file = File.read @app_config_path
|
330
|
+
data = JSON.load file
|
331
|
+
|
332
|
+
last_pull_time = data["LAST_PULL"] ? data["LAST_PULL"]["time"] : nil
|
333
|
+
last_pull_loc = data["LAST_PULL"] ? data["LAST_PULL"]["loc"] : nil
|
334
|
+
|
335
|
+
Whirly.start spinner: ["😺", "😸", "😹", "😻", "😼", "😽", "🙀", "😿", "😾"], status: "Retrieving files for #{@pastel.bold @sitename}" if quiet
|
336
|
+
resp = @client.pull @sitename, last_pull_time, last_pull_loc, quiet
|
337
|
+
|
338
|
+
# write last pull data to file (not necessarily the best way to do this, but better than cloning every time)
|
339
|
+
data["LAST_PULL"] = {
|
340
|
+
"time": Time.now,
|
341
|
+
"loc": Dir.pwd
|
342
|
+
}
|
343
|
+
|
344
|
+
File.write @app_config_path, data.to_json
|
345
|
+
rescue StandardError => ex
|
346
|
+
Whirly.stop if quiet
|
347
|
+
puts @pastel.red.bold "\nA fatal error occurred :-("
|
348
|
+
puts @pastel.red ex
|
349
|
+
ensure
|
350
|
+
exit
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def pizza
|
355
|
+
display_pizza_help_and_exit
|
356
|
+
end
|
357
|
+
|
358
|
+
def display_pizza_help_and_exit
|
359
|
+
puts "Sorry, we're fresh out of dough today. Try again tomorrow."
|
360
|
+
exit
|
361
|
+
end
|
362
|
+
|
363
|
+
def display_list_help_and_exit
|
364
|
+
display_banner
|
365
|
+
|
366
|
+
puts <<HERE
|
367
|
+
#{@pastel.green.bold 'list'} - List files on your Neocities site
|
368
|
+
|
369
|
+
#{@pastel.dim 'Examples:'}
|
370
|
+
|
371
|
+
#{@pastel.green '$ neocities list /'} List files in your root directory
|
372
|
+
|
373
|
+
#{@pastel.green '$ neocities list -a'} Recursively display all files and directories
|
374
|
+
|
375
|
+
#{@pastel.green '$ neocities list -d /mydir'} Show detailed information on /mydir
|
376
|
+
|
377
|
+
HERE
|
378
|
+
exit
|
379
|
+
end
|
380
|
+
|
381
|
+
def display_delete_help_and_exit
|
382
|
+
display_banner
|
383
|
+
|
384
|
+
puts <<HERE
|
385
|
+
#{@pastel.green.bold 'delete'} - Delete files on your Neocities site
|
386
|
+
|
387
|
+
#{@pastel.dim 'Examples:'}
|
388
|
+
|
389
|
+
#{@pastel.green '$ neocities delete myfile.jpg'} Delete myfile.jpg
|
390
|
+
|
391
|
+
#{@pastel.green '$ neocities delete myfile.jpg myfile2.jpg'} Delete myfile.jpg and myfile2.jpg
|
392
|
+
|
393
|
+
#{@pastel.green '$ neocities delete mydir'} Deletes mydir and everything inside it (be careful!)
|
394
|
+
|
395
|
+
HERE
|
396
|
+
exit
|
397
|
+
end
|
398
|
+
|
399
|
+
def display_upload_help_and_exit
|
400
|
+
display_banner
|
401
|
+
|
402
|
+
puts <<HERE
|
403
|
+
#{@pastel.green.bold 'upload'} - Upload individual files to your Neocities site
|
404
|
+
|
405
|
+
#{@pastel.dim 'Examples:'}
|
406
|
+
|
407
|
+
#{@pastel.green '$ neocities upload img.jpg img2.jpg'} Upload images to the root of your site
|
408
|
+
|
409
|
+
#{@pastel.green '$ neocities upload -d images img.jpg'} Upload img.jpg to the 'images' directory on your site
|
410
|
+
|
411
|
+
HERE
|
412
|
+
exit
|
413
|
+
end
|
414
|
+
|
415
|
+
def display_pull_help_and_exit
|
416
|
+
display_banner
|
417
|
+
|
418
|
+
puts <<HERE
|
419
|
+
#{@pastel.magenta.bold 'pull'} - Get the most recent version of files from your site
|
420
|
+
|
421
|
+
HERE
|
422
|
+
exit
|
423
|
+
end
|
424
|
+
|
425
|
+
def display_push_help_and_exit
|
426
|
+
display_banner
|
427
|
+
|
428
|
+
puts <<HERE
|
429
|
+
#{@pastel.green.bold 'push'} - Recursively upload a local directory to your Neocities site
|
430
|
+
|
431
|
+
#{@pastel.dim 'Examples:'}
|
432
|
+
|
433
|
+
#{@pastel.green '$ neocities push .'} Recursively upload current directory.
|
434
|
+
|
435
|
+
#{@pastel.green '$ neocities push -e node_modules -e secret.txt .'} Exclude certain files from push
|
436
|
+
|
437
|
+
#{@pastel.green '$ neocities push --no-gitignore .'} Don't use .gitignore to exclude files
|
438
|
+
|
439
|
+
#{@pastel.green '$ neocities push --dry-run .'} Just show what would be uploaded
|
440
|
+
|
441
|
+
#{@pastel.green '$ neocities push --prune .'} Delete site files not in dir (be careful!)
|
442
|
+
|
443
|
+
HERE
|
444
|
+
exit
|
445
|
+
end
|
446
|
+
|
447
|
+
def display_info_help_and_exit
|
448
|
+
display_banner
|
449
|
+
|
450
|
+
puts <<HERE
|
451
|
+
#{@pastel.green.bold 'info'} - Get site info
|
452
|
+
|
453
|
+
#{@pastel.dim 'Examples:'}
|
454
|
+
|
455
|
+
#{@pastel.green '$ neocities info fauux'} Gets info for 'fauux' site
|
456
|
+
|
457
|
+
HERE
|
458
|
+
exit
|
459
|
+
end
|
460
|
+
|
461
|
+
def display_logout_help_and_exit
|
462
|
+
display_banner
|
463
|
+
|
464
|
+
puts <<HERE
|
465
|
+
#{@pastel.green.bold 'logout'} - Remove the site api key from the config
|
466
|
+
|
467
|
+
#{@pastel.dim 'Examples:'}
|
468
|
+
|
469
|
+
#{@pastel.green '$ neocities logout -y'}
|
470
|
+
|
471
|
+
HERE
|
472
|
+
exit
|
473
|
+
end
|
474
|
+
|
475
|
+
def display_banner
|
476
|
+
puts <<HERE
|
477
|
+
|
478
|
+
|\\---/|
|
479
|
+
| #{PENELOPE_EYES.sample}_#{PENELOPE_EYES.sample} | #{@pastel.on_cyan.bold ' Neocities '}
|
480
|
+
\\_#{PENELOPE_MOUTHS.sample}_/
|
481
|
+
|
482
|
+
HERE
|
483
|
+
end
|
484
|
+
|
485
|
+
def display_help_and_exit
|
486
|
+
display_banner
|
487
|
+
puts <<HERE
|
488
|
+
#{@pastel.dim 'Subcommands:'}
|
489
|
+
push Recursively upload a local directory to your site
|
490
|
+
upload Upload individual files to your Neocities site
|
491
|
+
delete Delete files from your Neocities site
|
492
|
+
list List files from your Neocities site
|
493
|
+
info Information and stats for your site
|
494
|
+
logout Remove the site api key from the config
|
495
|
+
version Unceremoniously display version and self destruct
|
496
|
+
pull Get the most recent version of files from your site
|
497
|
+
pizza Order a free pizza
|
498
|
+
|
499
|
+
HERE
|
500
|
+
exit
|
501
|
+
end
|
502
|
+
|
503
|
+
def self.app_config_path(name)
|
504
|
+
platform = if RUBY_PLATFORM =~ /win32/
|
505
|
+
:win32
|
506
|
+
elsif RUBY_PLATFORM =~ /darwin/
|
507
|
+
:darwin
|
508
|
+
elsif RUBY_PLATFORM =~ /linux/
|
509
|
+
:linux
|
510
|
+
else
|
511
|
+
:unknown
|
512
|
+
end
|
513
|
+
|
514
|
+
case platform
|
515
|
+
when :linux
|
516
|
+
if ENV['XDG_CONFIG_HOME']
|
517
|
+
return File.join(ENV['XDG_CONFIG_HOME'], name)
|
518
|
+
end
|
519
|
+
|
520
|
+
if ENV['HOME']
|
521
|
+
return File.join(ENV['HOME'], '.config', name)
|
522
|
+
end
|
523
|
+
when :darwin
|
524
|
+
return File.join(ENV['HOME'], 'Library', 'Application Support', name)
|
525
|
+
else
|
526
|
+
# Windows platform detection is weird, just look for the env variables
|
527
|
+
if ENV['LOCALAPPDATA']
|
528
|
+
return File.join(ENV['LOCALAPPDATA'], name)
|
529
|
+
end
|
530
|
+
|
531
|
+
if ENV['USERPROFILE']
|
532
|
+
return File.join(ENV['USERPROFILE'], 'Local Settings', 'Application Data', name)
|
533
|
+
end
|
534
|
+
|
535
|
+
# Should work for the BSDs
|
536
|
+
if ENV['HOME']
|
537
|
+
return File.join(ENV['HOME'], '.'+name)
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
begin
|
2
|
+
require 'openssl/win/root' if Gem.win_platform?
|
3
|
+
rescue
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'json'
|
7
|
+
require 'pathname'
|
8
|
+
require 'uri'
|
9
|
+
require 'digest'
|
10
|
+
require 'httpclient'
|
11
|
+
require 'pastel'
|
12
|
+
require 'date'
|
13
|
+
|
14
|
+
require 'whirly'
|
15
|
+
|
16
|
+
module Neocities
|
17
|
+
class Client
|
18
|
+
API_URI = 'https://neocities.org/api/'
|
19
|
+
|
20
|
+
def initialize(opts={})
|
21
|
+
@uri = URI.parse API_URI
|
22
|
+
@http = HTTPClient.new force_basic_auth: true
|
23
|
+
@opts = opts
|
24
|
+
@pastel = Pastel.new eachline: "\n"
|
25
|
+
|
26
|
+
unless opts[:api_key] || (opts[:sitename] && opts[:password])
|
27
|
+
raise ArgumentError, 'client requires a login (sitename/password) or an api_key'
|
28
|
+
end
|
29
|
+
|
30
|
+
if opts[:api_key]
|
31
|
+
@http.default_header = {'Authorization' => "Bearer #{opts[:api_key]}"}
|
32
|
+
else
|
33
|
+
@http.set_auth API_URI, opts[:sitename], opts[:password]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def list(path=nil)
|
38
|
+
get 'list', :path => path
|
39
|
+
end
|
40
|
+
|
41
|
+
def pull(sitename, last_pull_time=nil, last_pull_loc=nil, quiet=true)
|
42
|
+
site_info = get 'info', sitename: sitename
|
43
|
+
|
44
|
+
if site_info[:result] == 'error'
|
45
|
+
raise ArgumentError, site_info[:message]
|
46
|
+
end
|
47
|
+
|
48
|
+
# handle custom domains for supporter accounts
|
49
|
+
if site_info[:info][:domain] && site_info[:info][:domain] != ""
|
50
|
+
domain = "https://#{site_info[:info][:domain]}/"
|
51
|
+
else
|
52
|
+
domain = "https://#{sitename}.neocities.org/"
|
53
|
+
end
|
54
|
+
|
55
|
+
# start stats
|
56
|
+
success_loaded = 0
|
57
|
+
start_time = Time.now
|
58
|
+
curr_dir = Dir.pwd
|
59
|
+
|
60
|
+
# get list of files
|
61
|
+
resp = get 'list'
|
62
|
+
|
63
|
+
if resp[:result] == 'error'
|
64
|
+
raise ArgumentError, resp[:message]
|
65
|
+
end
|
66
|
+
|
67
|
+
# fetch each file
|
68
|
+
uri_parser = URI::Parser.new
|
69
|
+
resp[:files].each do |file|
|
70
|
+
if !file[:is_directory]
|
71
|
+
print @pastel.bold("Loading #{file[:path]} ... ") if !quiet
|
72
|
+
|
73
|
+
if
|
74
|
+
last_pull_time && \
|
75
|
+
last_pull_loc && \
|
76
|
+
Time.parse(file[:updated_at]) <= Time.parse(last_pull_time) && \
|
77
|
+
last_pull_loc == curr_dir && \
|
78
|
+
File.exist?(file[:path]) # case when user deletes file
|
79
|
+
# case when file hasn't been updated since last
|
80
|
+
print "#{@pastel.yellow.bold "NO NEW UPDATES"}\n" if !quiet
|
81
|
+
next
|
82
|
+
end
|
83
|
+
|
84
|
+
pathtotry = uri_parser.escape(domain + file[:path])
|
85
|
+
fileconts = @http.get pathtotry
|
86
|
+
|
87
|
+
# follow redirects
|
88
|
+
while fileconts.status == 301
|
89
|
+
new_path = fileconts.header['location'][0]
|
90
|
+
print "\n#{@pastel.red "Fetch from #{pathtotry} failed."}\nTrying #{new_path} instead..." if !quiet
|
91
|
+
|
92
|
+
pathtotry = new_path
|
93
|
+
fileconts = @http.get pathtotry
|
94
|
+
end
|
95
|
+
|
96
|
+
if fileconts.ok?
|
97
|
+
print "#{@pastel.green.bold 'SUCCESS'}\n" if !quiet
|
98
|
+
success_loaded += 1
|
99
|
+
|
100
|
+
File.open("#{file[:path]}", "w") do |f|
|
101
|
+
f.write(fileconts.body)
|
102
|
+
end
|
103
|
+
else
|
104
|
+
print "#{@pastel.red.bold 'FAIL'}\n" if !quiet
|
105
|
+
end
|
106
|
+
else
|
107
|
+
FileUtils.mkdir_p "#{file[:path]}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# calculate time command took
|
112
|
+
total_time = Time.now - start_time
|
113
|
+
|
114
|
+
# stop the spinner, if there is one
|
115
|
+
Whirly.stop if quiet
|
116
|
+
|
117
|
+
# display stats
|
118
|
+
puts @pastel.green "\nSuccessfully fetched #{success_loaded} files in #{total_time} seconds"
|
119
|
+
end
|
120
|
+
|
121
|
+
def key
|
122
|
+
get 'key'
|
123
|
+
end
|
124
|
+
|
125
|
+
def upload_hash(remote_path, sha1_hash)
|
126
|
+
post 'upload_hash', remote_path => sha1_hash
|
127
|
+
end
|
128
|
+
|
129
|
+
def upload(path, remote_path=nil, dry_run=false)
|
130
|
+
path = Pathname path
|
131
|
+
|
132
|
+
unless path.exist?
|
133
|
+
raise ArgumentError, "#{path.to_s} does not exist."
|
134
|
+
end
|
135
|
+
|
136
|
+
rpath = (remote_path || path.basename)
|
137
|
+
|
138
|
+
res = upload_hash rpath, Digest::SHA1.file(path.to_s).hexdigest
|
139
|
+
|
140
|
+
if res[:files] && res[:files][remote_path.to_s.to_sym] == true
|
141
|
+
return {result: 'error', error_type: 'file_exists', message: 'file already exists and matches local file, not uploading'}
|
142
|
+
else
|
143
|
+
if dry_run
|
144
|
+
return {result: 'success'}
|
145
|
+
else
|
146
|
+
File.open(path.to_s) do |file|
|
147
|
+
post 'upload', rpath => file
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def delete_wrapper_with_dry_run(paths, dry_run=false)
|
154
|
+
if dry_run
|
155
|
+
return {result: 'success'}
|
156
|
+
else
|
157
|
+
delete(paths)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def delete(*paths)
|
162
|
+
post 'delete', 'filenames[]' => paths
|
163
|
+
end
|
164
|
+
|
165
|
+
def info(sitename)
|
166
|
+
get 'info', sitename: sitename
|
167
|
+
end
|
168
|
+
|
169
|
+
def get(path, params={})
|
170
|
+
uri = @uri+path
|
171
|
+
uri.query = URI.encode_www_form params
|
172
|
+
resp = @http.get uri
|
173
|
+
|
174
|
+
JSON.parse resp.body, symbolize_names: true
|
175
|
+
end
|
176
|
+
|
177
|
+
def post(path, args={})
|
178
|
+
uri = @uri+path
|
179
|
+
resp = @http.post uri, args
|
180
|
+
JSON.parse resp.body, symbolize_names: true
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
data/lib/neocities/version.rb
CHANGED
data/lib/neocities.rb
ADDED
data/neocities.gemspec
CHANGED
@@ -7,15 +7,20 @@ Gem::Specification.new do |spec|
|
|
7
7
|
spec.name = "neocities"
|
8
8
|
spec.version = Neocities::VERSION
|
9
9
|
spec.authors = ["Kyle Drake"]
|
10
|
-
spec.email = ["
|
11
|
-
spec.summary = %q{
|
10
|
+
spec.email = ["contact@neocities.org"]
|
11
|
+
spec.summary = %q{Neocities.org CLI and API client}
|
12
12
|
spec.homepage = "https://neocities.org"
|
13
13
|
spec.license = "MIT"
|
14
|
-
spec.files = `git ls-files -
|
15
|
-
spec.executables =
|
16
|
-
spec.test_files = spec.files.grep(%r{^(
|
14
|
+
spec.files = `git ls-files | grep -Ev '^(test)'`.split("\n")
|
15
|
+
spec.executables = ['neocities']
|
16
|
+
spec.test_files = spec.files.grep(%r{^(tests)/})
|
17
17
|
spec.require_paths = ["lib"]
|
18
|
+
spec.extensions = ['ext/mkrf_conf.rb']
|
18
19
|
|
19
|
-
spec.
|
20
|
-
spec.
|
20
|
+
spec.add_dependency 'tty-table', '~> 0.10', '= 0.10.0'
|
21
|
+
spec.add_dependency 'tty-prompt', '~> 0.12', '= 0.12.0'
|
22
|
+
spec.add_dependency 'pastel', '~> 0.7', '= 0.7.2'
|
23
|
+
spec.add_dependency 'httpclient-fixcerts', '~> 2.8', '>= 2.8.5'
|
24
|
+
spec.add_dependency 'rake', '~> 12.3', '>= 12.3.1'
|
25
|
+
spec.add_dependency 'whirly', '~> 0.3', '>= 0.3.0'
|
21
26
|
end
|
metadata
CHANGED
@@ -1,56 +1,152 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: neocities
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.20
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kyle Drake
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-07-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: tty-table
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
20
|
-
|
19
|
+
version: '0.10'
|
20
|
+
- - '='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.10.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.10'
|
30
|
+
- - '='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.10.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: tty-prompt
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.12'
|
40
|
+
- - '='
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 0.12.0
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0.12'
|
50
|
+
- - '='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 0.12.0
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: pastel
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0.7'
|
60
|
+
- - '='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 0.7.2
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0.7'
|
70
|
+
- - '='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 0.7.2
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: httpclient-fixcerts
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - "~>"
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '2.8'
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 2.8.5
|
83
|
+
type: :runtime
|
21
84
|
prerelease: false
|
22
85
|
version_requirements: !ruby/object:Gem::Requirement
|
23
86
|
requirements:
|
24
87
|
- - "~>"
|
25
88
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
89
|
+
version: '2.8'
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 2.8.5
|
27
93
|
- !ruby/object:Gem::Dependency
|
28
94
|
name: rake
|
29
95
|
requirement: !ruby/object:Gem::Requirement
|
30
96
|
requirements:
|
97
|
+
- - "~>"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '12.3'
|
31
100
|
- - ">="
|
32
101
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
34
|
-
type: :
|
102
|
+
version: 12.3.1
|
103
|
+
type: :runtime
|
35
104
|
prerelease: false
|
36
105
|
version_requirements: !ruby/object:Gem::Requirement
|
37
106
|
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '12.3'
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 12.3.1
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
name: whirly
|
115
|
+
requirement: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - "~>"
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0.3'
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: 0.3.0
|
123
|
+
type: :runtime
|
124
|
+
prerelease: false
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - "~>"
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0.3'
|
38
130
|
- - ">="
|
39
131
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
132
|
+
version: 0.3.0
|
41
133
|
description:
|
42
134
|
email:
|
43
|
-
-
|
44
|
-
executables:
|
45
|
-
|
135
|
+
- contact@neocities.org
|
136
|
+
executables:
|
137
|
+
- neocities
|
138
|
+
extensions:
|
139
|
+
- ext/mkrf_conf.rb
|
46
140
|
extra_rdoc_files: []
|
47
141
|
files:
|
48
142
|
- ".gitignore"
|
49
143
|
- Gemfile
|
50
|
-
- LICENSE.txt
|
51
144
|
- README.md
|
52
|
-
-
|
53
|
-
-
|
145
|
+
- bin/neocities
|
146
|
+
- ext/mkrf_conf.rb
|
147
|
+
- lib/neocities.rb
|
148
|
+
- lib/neocities/cli.rb
|
149
|
+
- lib/neocities/client.rb
|
54
150
|
- lib/neocities/version.rb
|
55
151
|
- neocities.gemspec
|
56
152
|
homepage: https://neocities.org
|
@@ -72,9 +168,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
168
|
- !ruby/object:Gem::Version
|
73
169
|
version: '0'
|
74
170
|
requirements: []
|
75
|
-
|
76
|
-
rubygems_version: 2.2.2
|
171
|
+
rubygems_version: 3.5.13
|
77
172
|
signing_key:
|
78
173
|
specification_version: 4
|
79
|
-
summary:
|
174
|
+
summary: Neocities.org CLI and API client
|
80
175
|
test_files: []
|
data/LICENSE.txt
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
Copyright (c) 2014 Kyle Drake
|
2
|
-
|
3
|
-
MIT License
|
4
|
-
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
-
a copy of this software and associated documentation files (the
|
7
|
-
"Software"), to deal in the Software without restriction, including
|
8
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
-
permit persons to whom the Software is furnished to do so, subject to
|
11
|
-
the following conditions:
|
12
|
-
|
13
|
-
The above copyright notice and this permission notice shall be
|
14
|
-
included in all copies or substantial portions of the Software.
|
15
|
-
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
require "bundler/gem_tasks"
|
data/lib/neocities/neocities.rb
DELETED