cdnget 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/cdnget CHANGED
@@ -2,29 +2,97 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  ##
5
- ## Download files from CDN (CDNJS, Google, jsDelivr).
5
+ ## Download files from CDN (CDNJS, jsDelivr, UNPKG, Google).
6
6
  ##
7
7
  ## - CDNJS (https://cdnjs.com/)
8
- ## - Google (https://developers.google.com/speed/libraries/)
9
8
  ## - jsDelivr (https://www.jsdelivr.com/)
9
+ ## - UNPKG (https://unpkg.com/)
10
+ ## - Google (https://developers.google.com/speed/libraries/)
10
11
  ##
11
12
  ## Example:
12
13
  ## $ cdnget # list public CDN
13
14
  ## $ cdnget [-q] cdnjs # list libraries
14
15
  ## $ cdnget [-q] cdnjs jquery # list versions
15
- ## $ cdnget [-q] cdnjs jquery 2.2.0 # list files
16
- ## $ cdnget [-q] cdnjs jquery 2.2.0 /tmp # download files
16
+ ## $ cdnget [-q] cdnjs jquery latest # detect latest version
17
+ ## $ cdnget [-q] cdnjs jquery 3.6.0 # list files
18
+ ## $ cdnget [-q] cdnjs jquery 3.6.0 /tmp # download files
17
19
  ##
18
20
 
19
21
  require 'open-uri'
22
+ require 'uri'
23
+ require 'net/http'
24
+ require 'openssl'
20
25
  require 'json'
21
26
  require 'fileutils'
27
+ require 'pp'
22
28
 
23
29
 
24
30
  module CDNGet
25
31
 
26
32
 
27
- RELEASE = '$Release: 0.1.0 $'.split()[1]
33
+ RELEASE = '$Release: 1.0.0 $'.split()[1]
34
+
35
+
36
+ class HttpConnection
37
+
38
+ def initialize(uri, headers=nil)
39
+ http = Net::HTTP.new(uri.host, uri.port)
40
+ if uri.scheme == 'https'
41
+ http.use_ssl = true
42
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
43
+ end
44
+ http.start()
45
+ @http = http
46
+ @headers = headers
47
+ end
48
+
49
+ def self.open(uri, headers=nil)
50
+ http = self.new(uri, headers)
51
+ return http unless block_given?()
52
+ begin
53
+ return yield http
54
+ ensure
55
+ http.close()
56
+ end
57
+ end
58
+
59
+ def get(uri)
60
+ resp = @http.send_request('GET', uri.path, nil, @headers)
61
+ case resp
62
+ when Net::HTTPSuccess
63
+ return resp.body
64
+ #when HTTPInformation, Net::HTTPRedirection, HTTPClientError, HTTPServerError
65
+ else
66
+ raise HttpError.new(resp.code.to_i, resp.message)
67
+ end
68
+ end
69
+
70
+ def post(uri, payload)
71
+ path = uri.path
72
+ path += "?"+uri.query if uri.query && !uri.query.empty?
73
+ resp = @http.send_request('POST', path, payload, @headers)
74
+ case resp
75
+ when Net::HTTPSuccess ; return resp.body
76
+ else ; raise HttpError.new(resp.code.to_i, resp.message)
77
+ end
78
+ end
79
+
80
+ def close()
81
+ @http.finish()
82
+ end
83
+
84
+ end
85
+
86
+
87
+ class HttpError < StandardError
88
+ def initialize(code, msgtext)
89
+ super("#{code} #{msgtext}")
90
+ @code = code
91
+ @msgtext = msgtext
92
+ end
93
+ attr_reader :code, :msgtext
94
+ end
95
+
28
96
 
29
97
  CLASSES = []
30
98
 
@@ -35,10 +103,22 @@ module CDNGet
35
103
  CLASSES << klass
36
104
  end
37
105
 
106
+ def initialize(debug_mode: false)
107
+ @debug_mode = debug_mode
108
+ end
109
+ attr_reader :debug_mode
110
+
38
111
  def list()
39
112
  raise NotImplementedError.new("#{self.class.name}#list(): not implemented yet.")
40
113
  end
41
114
 
115
+ def search(pattern)
116
+ return list().select {|d| File.fnmatch(pattern, d[:name], File::FNM_CASEFOLD) }
117
+ #rexp_str = pattern.split('*', -1).collect {|x| Regexp.escape(x) }.join('.*')
118
+ #rexp = Regexp.compile("\\A#{rexp_str}\\z", Regexp::IGNORECASE)
119
+ #return list().select {|d| d[:name] =~ rexp }
120
+ end
121
+
42
122
  def find(library)
43
123
  raise NotImplementedError.new("#{self.class.name}#find(): not implemented yet.")
44
124
  end
@@ -53,14 +133,28 @@ module CDNGet
53
133
  raise CommandError.new("#{basedir}: not exist.")
54
134
  File.directory?(basedir) or
55
135
  raise CommandError.new("#{basedir}: not a directory.")
56
- target_dir = File.join(basedir, library, version)
57
136
  d = get(library, version)
137
+ target_dir = d[:destdir] ? File.join(basedir, d[:destdir]) \
138
+ : File.join(basedir, library, version)
139
+ http = nil
58
140
  d[:files].each do |file|
59
141
  filepath = File.join(target_dir, file)
142
+ if filepath.end_with?('/')
143
+ if File.exist?(filepath)
144
+ puts "#{filepath} ... Done (Already exists)" unless quiet
145
+ else
146
+ print "#{filepath} ..." unless quiet
147
+ FileUtils.mkdir_p(filepath)
148
+ puts " Done (Created)" unless quiet
149
+ end
150
+ next
151
+ end
60
152
  dirpath = File.dirname(filepath)
61
153
  print "#{filepath} ..." unless quiet
62
154
  url = File.join(d[:baseurl], file) # not use URI.join!
63
- content = fetch(url)
155
+ uri = URI.parse(url)
156
+ http ||= HttpConnection.new(uri)
157
+ content = http.get(uri)
64
158
  content = content.force_encoding('ascii-8bit')
65
159
  print " Done (#{format_integer(content.bytesize)} byte)" unless quiet
66
160
  FileUtils.mkdir_p(dirpath) unless File.exist?(dirpath)
@@ -72,17 +166,32 @@ module CDNGet
72
166
  end
73
167
  puts() unless quiet
74
168
  end
169
+ http.close() if http
75
170
  nil
76
171
  end
77
172
 
78
173
  protected
79
174
 
175
+ def http_get(url)
176
+ ## * `open()` on Ruby 3.X can't open http url
177
+ ## * `URI.open()` on Ruby <= 2.4 raises NoMethodError (private method `open' called)
178
+ ## * `URI.__send__(:open)` is a hack to work on both Ruby 2.X and 3.X
179
+ return URI.__send__(:open, url, 'rb') {|f| f.read() }
180
+ end
181
+
80
182
  def fetch(url, library=nil)
81
183
  begin
82
- html = open(url, 'rb') {|f| f.read() }
184
+ html = http_get(url)
83
185
  return html
84
- rescue OpenURI::HTTPError => ex
85
- raise CommandError.new("GET #{url} : #{ex.message}")
186
+ rescue OpenURI::HTTPError => exc
187
+ if ! (exc.message == "404 Not Found" && library)
188
+ raise CommandError.new("GET #{url} : #{exc.message}")
189
+ elsif ! library.end_with?('js')
190
+ raise CommandError.new("#{library}: Library not found.")
191
+ else
192
+ maybe = library.end_with?('.js') ? library.sub('.js', 'js') : library.sub(/js$/, '.js')
193
+ raise CommandError.new("#{library}: Library not found (maybe '#{maybe}'?).")
194
+ end
86
195
  end
87
196
  end
88
197
 
@@ -101,82 +210,80 @@ module CDNGet
101
210
  return value.to_s.reverse.scan(/..?.?/).collect {|s| s.reverse }.reverse.join(',')
102
211
  end
103
212
 
104
- end
105
-
213
+ def _debug_print(x)
214
+ if @debug_mode
215
+ $stderr.puts "\e[0;35m*** #{PP.pp(x,'')}\e[0m"
216
+ end
217
+ end
106
218
 
107
- class HttpError < StandardError
108
219
  end
109
220
 
110
221
 
111
- class CDNJS < Base # TODO: use jsdelivr api
222
+ class CDNJS < Base
112
223
  CODE = "cdnjs"
113
224
  SITE_URL = 'https://cdnjs.com/'
114
225
 
115
226
  def fetch(url, library=nil)
116
- begin
117
- html = open(url, 'rb') {|f| f.read() }
118
- if library
119
- html =~ /<h1\b.*?>(.*?)<\/h1>/ or
120
- raise CommandError.new("#{library}: Library not found.")
121
- library == $1.strip() or
122
- raise CommandError.new("#{library}: Library not found (maybe '#{$1.strip()}'?).")
227
+ json_str = super
228
+ if json_str == "{}" && library
229
+ if library.end_with?('js')
230
+ maybe = library.end_with?('.js') \
231
+ ? library.sub('.js', 'js') \
232
+ : library.sub(/js$/, '.js')
233
+ raise CommandError.new("#{library}: Library not found (maybe '#{maybe}'?).")
234
+ else
235
+ raise CommandError.new("#{library}: Library not found.")
123
236
  end
124
- return html
125
- rescue OpenURI::HTTPError => ex
126
- raise HttpError.new("GET #{url} : #{ex.message}")
127
237
  end
238
+ return json_str
128
239
  end
129
240
  protected :fetch
130
241
 
131
- def list
132
- libs = []
133
- html = fetch("https://cdnjs.com/libraries")
134
- html.scan(/<td\b(.*?)<\/td>/m) do |str,|
135
- name = desc = nil
136
- name = $1 if str =~ /href="\/libraries\/([-.\w]+)">\s*\1\s*<\/a>/
137
- desc = $1.strip if str =~ /<div style="display: none;">(.*?)<\/div>/m
138
- libs << {name: name, desc: desc} if name
139
- end
242
+ def list()
243
+ jstr = fetch("https://api.cdnjs.com/libraries?fields=name,description")
244
+ jdata = JSON.parse(jstr)
245
+ _debug_print(jdata)
246
+ libs = jdata['results'].collect {|d| {name: d['name'], desc: d['description']} }
140
247
  return libs.sort_by {|d| d[:name] }.uniq
141
248
  end
142
249
 
143
250
  def find(library)
144
251
  validate(library, nil)
145
- html = fetch("https://cdnjs.com/libraries/#{library}", library)
146
- versions = []
147
- html.scan(/<option value="([^"]+)" *(?:selected)?>/) do |ver,|
148
- versions << ver
149
- end
150
- desc = tags = nil
151
- if html =~ /<p>(.*?)<\/p>\s*<em>(.*?)<\/em>/
152
- desc = $1
153
- tags = $2
154
- end
252
+ jstr = fetch("https://api.cdnjs.com/libraries/#{library}", library)
253
+ jdata = JSON.parse(jstr)
254
+ _debug_print(jdata)
255
+ versions = jdata['assets'].collect {|d| d['version'] }\
256
+ .sort_by {|v| v.split(/[-.]/).map(&:to_i) }
155
257
  return {
156
258
  name: library,
157
- desc: desc,
158
- tags: tags,
159
- versions: versions,
259
+ desc: jdata['description'],
260
+ tags: (jdata['keywords'] || []).join(", "),
261
+ site: jdata['homepage'],
262
+ info: File.join(SITE_URL, "/libraries/#{library}"),
263
+ license: jdata['license'],
264
+ versions: versions.reverse(),
160
265
  }
161
266
  end
162
267
 
163
268
  def get(library, version)
164
269
  validate(library, version)
165
- html = fetch("https://cdnjs.com/libraries/#{library}/#{version}", library)
166
- baseurl = "https://cdnjs.cloudflare.com/ajax/libs/#{library}/#{version}"
167
- urls = []
168
- files = []
169
- rexp = %r`>(#{Regexp.escape(baseurl)}/([^<]+))<\/p>`
170
- html.scan(%r`>(#{Regexp.escape(baseurl)}/([^<]+))<\/p>`) do |url, file|
171
- urls << url .gsub(/&#x2F;/, '/')
172
- files << file.gsub(/&#x2F;/, '/')
173
- end
270
+ jstr = fetch("https://api.cdnjs.com/libraries/#{library}", library)
271
+ jdata = JSON.parse(jstr)
272
+ _debug_print(jdata)
273
+ d = jdata['assets'].find {|d| d['version'] == version } or
274
+ raise CommandError.new("#{library}/#{version}: Library or version not found.")
275
+ baseurl = "https://cdnjs.cloudflare.com/ajax/libs/#{library}/#{version}/"
174
276
  return {
175
277
  name: library,
176
278
  version: version,
177
- urls: urls,
178
- files: files,
279
+ desc: jdata['description'],
280
+ tags: (jdata['keywords'] || []).join(", "),
281
+ site: jdata['homepage'],
282
+ info: File.join(SITE_URL, "/libraries/#{library}/#{version}"),
283
+ urls: d['files'].collect {|s| baseurl + s },
284
+ files: d['files'],
179
285
  baseurl: baseurl,
286
+ license: jdata['license'],
180
287
  }
181
288
  end
182
289
 
@@ -186,59 +293,200 @@ module CDNGet
186
293
  class JSDelivr < Base
187
294
  CODE = "jsdelivr"
188
295
  SITE_URL = "https://www.jsdelivr.com/"
189
- API_URL = "https://api.jsdelivr.com/v1/jsdelivr/libraries"
296
+ #API_URL = "https://api.jsdelivr.com/v1/jsdelivr/libraries"
297
+ API_URL = "https://data.jsdelivr.com/v1"
298
+ HEADERS = {
299
+ "x-algo""lia-app""lication-id"=>"OFCNC""OG2CU",
300
+ "x-algo""lia-api""-key"=>"f54e21fa3a2""a0160595bb05""8179bfb1e",
301
+ }
190
302
 
191
- def list
192
- json = fetch("#{API_URL}?fields=name,description,homepage")
193
- arr = JSON.load(json)
194
- return arr.collect {|d|
195
- {name: d["name"], desc: d["description"], site: d["homepage"] }
303
+ def list()
304
+ return nil # nil means that this CDN can't list libraries without pattern
305
+ end
306
+
307
+ def search(pattern)
308
+ form_data = {
309
+ query: pattern,
310
+ page: "0",
311
+ hitsPerPage: "1000",
312
+ attributesToHighlight: '[]',
313
+ attributesToRetrieve: '["name","description","version"]'
314
+ }
315
+ payload = JSON.dump({"params"=>URI.encode_www_form(form_data)})
316
+ url = "https://ofcncog2cu-3.algolianet.com/1/indexes/npm-search/query"
317
+ uri = URI.parse(url)
318
+ json = HttpConnection.open(uri, HEADERS) {|http| http.post(uri, payload) }
319
+ jdata = JSON.load(json)
320
+ _debug_print(jdata)
321
+ return jdata["hits"].select {|d|
322
+ File.fnmatch(pattern, d["name"], File::FNM_CASEFOLD)
323
+ }.collect {|d|
324
+ {name: d["name"], desc: d["description"], version: d["version"]}
196
325
  }
197
326
  end
198
327
 
199
328
  def find(library)
200
329
  validate(library, nil)
201
- json = fetch("#{API_URL}?name=#{library}&fields=name,description,homepage,versions")
202
- arr = JSON.load(json)
203
- d = arr.first or
204
- raise CommandError.new("#{library}: Library not found.")
330
+ url = "https://ofcncog2cu-dsn.algolia.net/1/indexes/npm-search/#{library}"
331
+ uri = URI.parse(url)
332
+ begin
333
+ json = HttpConnection.open(uri, HEADERS) {|http| http.get(uri) }
334
+ rescue HttpError
335
+ raise CommandError, "#{library}: Library not found."
336
+ end
337
+ dict1 = JSON.load(json)
338
+ _debug_print(dict1)
339
+ #
340
+ json = fetch("#{API_URL}/package/npm/#{library}")
341
+ dict2 = JSON.load(json)
342
+ _debug_print(dict2)
343
+ #
344
+ d = dict1
205
345
  return {
206
- name: d['name'],
207
- desc: d['description'],
208
- site: d['homepage'],
209
- versions: d['versions'],
346
+ name: d['name'],
347
+ desc: d['description'],
348
+ #versions: d['versions'].collect {|k,v| k },
349
+ versions: dict2['versions'],
350
+ tags: (d['keywords'] || []).join(", "),
351
+ site: d['homepage'],
352
+ info: File.join(SITE_URL, "/package/npm/#{library}"),
353
+ license: d['license'],
210
354
  }
211
355
  end
212
356
 
213
357
  def get(library, version)
214
358
  validate(library, version)
215
- baseurl = "https://cdn.jsdelivr.net/#{library}/#{version}"
216
- url = "#{API_URL}/#{library}/#{version}"
217
- json = fetch("#{API_URL}/#{library}/#{version}")
218
- files = JSON.load(json)
219
- ! files.empty? or
220
- raise CommandError.new("#{library}: Library not found.")
221
- urls = files.collect {|x| "#{baseurl}/#{x}" }
359
+ url = File.join(API_URL, "/package/npm/#{library}@#{version}/flat")
360
+ begin
361
+ json = fetch(url, library)
362
+ rescue CommandError
363
+ raise CommandError.new("#{library}@#{version}: Library or version not found.")
364
+ end
365
+ jdata = JSON.load(json)
366
+ files = jdata["files"].collect {|d| d["name"] }
367
+ baseurl = "https://cdn.jsdelivr.net/npm/#{library}@#{version}"
368
+ _debug_print(jdata)
369
+ #
370
+ dict = find(library)
371
+ dict.delete(:versions)
372
+ dict.update({
373
+ version: version,
374
+ info: File.join(SITE_URL, "/package/npm/#{library}?version=#{version}"),
375
+ npmpkg: "https://registry.npmjs.org/#{library}/-/#{library}-#{version}.tgz",
376
+ urls: files.collect {|x| baseurl + x },
377
+ files: files,
378
+ baseurl: baseurl,
379
+ default: jdata["default"],
380
+ destdir: "#{library}@#{version}",
381
+ })
382
+ return dict
383
+ end
384
+
385
+ end
386
+
387
+
388
+ class Unpkg < Base
389
+ CODE = "unpkg"
390
+ SITE_URL = "https://unpkg.com/"
391
+ #API_URL = "https://www.npmjs.com"
392
+ API_URL = "https://api.npms.io/v2"
393
+
394
+ protected
395
+
396
+ def http_get(url)
397
+ return URI.__send__(:open, url, 'rb', {"x-spiferack"=>"1"}) {|f| f.read() }
398
+ end
399
+
400
+ public
401
+
402
+ def list()
403
+ return nil # nil means that this CDN can't list libraries without pattern
404
+ end
405
+
406
+ def search(pattern)
407
+ #json = fetch("#{API_URL}/search?q=#{pattern}")
408
+ json = fetch("#{API_URL}/search?q=#{pattern}&size=250")
409
+ jdata = JSON.load(json)
410
+ _debug_print(jdata)
411
+ #arr = jdata["objects"] # www.npmjs.com
412
+ arr = jdata["results"] # api.npms.io
413
+ return arr.select {|dict|
414
+ File.fnmatch(pattern, dict["package"]["name"], File::FNM_CASEFOLD)
415
+ }.collect {|dict|
416
+ d = dict["package"]
417
+ {name: d["name"], desc: d["description"], version: d["version"]}
418
+ }
419
+ end
420
+
421
+ def find(library)
422
+ validate(library, nil)
423
+ json = fetch("#{API_URL}/package/#{library}", library)
424
+ jdata = JSON.load(json)
425
+ _debug_print(jdata)
426
+ dict = jdata["collected"]["metadata"]
427
+ versions = [dict["version"]]
428
+ #
429
+ url = File.join(SITE_URL, "/browse/#{library}/")
430
+ html = fetch(url, library)
431
+ _debug_print(html)
432
+ if html =~ /<script>window.__DATA__\s*=\s*(.*?)<\/script>/m
433
+ jdata2 = JSON.load($1)
434
+ versions = jdata2["availableVersions"].reverse()
435
+ end
436
+ #
222
437
  return {
438
+ name: dict["name"],
439
+ desc: dict["description"],
440
+ tags: (dict["keywords"] || []).join(", "),
441
+ site: dict["links"] ? dict["links"]["homepage"] : dict["links"]["npm"],
442
+ info: File.join(SITE_URL, "/browse/#{library}/"),
443
+ versions: versions,
444
+ license: dict["license"],
445
+ }
446
+ end
447
+
448
+ def get(library, version)
449
+ validate(library, version)
450
+ dict = find(library)
451
+ dict.delete(:versions)
452
+ #
453
+ url = "https://data.jsdelivr.com/v1/package/npm/#{library}@#{version}/flat"
454
+ begin
455
+ json = fetch(url, library)
456
+ rescue CommandError
457
+ raise CommandError.new("#{library}@#{version}: Library or version not found.")
458
+ end
459
+ jdata = JSON.load(json)
460
+ files = jdata["files"].collect {|d| d["name"] }
461
+ baseurl = File.join(SITE_URL, "/#{library}@#{version}")
462
+ _debug_print(jdata)
463
+ #
464
+ dict.update({
223
465
  name: library,
224
466
  version: version,
225
- urls: urls,
467
+ info: File.join(SITE_URL, "/browse/#{library}@#{version}/"),
468
+ npmpkg: "https://registry.npmjs.org/#{library}/-/#{library}-#{version}.tgz",
469
+ urls: files.collect {|x| baseurl+x },
226
470
  files: files,
227
471
  baseurl: baseurl,
228
- }
472
+ default: jdata["default"],
473
+ destdir: "#{library}@#{version}",
474
+ })
475
+ return dict
229
476
  end
230
477
 
231
478
  end
232
479
 
233
480
 
234
- class GoogleCDN < Base # TODO: use jsdelivr api
481
+ class GoogleCDN < Base
235
482
  CODE = "google"
236
483
  SITE_URL = 'https://developers.google.com/speed/libraries/'
237
484
 
238
- def list
239
- libs = []
485
+ def list()
240
486
  html = fetch("https://developers.google.com/speed/libraries/")
487
+ _debug_print(html)
241
488
  rexp = %r`"https://ajax\.googleapis\.com/ajax/libs/([^/]+)/([^/]+)/([^"]+)"`
489
+ libs = []
242
490
  html.scan(rexp) do |lib, ver, file|
243
491
  libs << {name: lib, desc: "latest version: #{ver}" }
244
492
  end
@@ -248,6 +496,7 @@ module CDNGet
248
496
  def find(library)
249
497
  validate(library, nil)
250
498
  html = fetch("https://developers.google.com/speed/libraries/")
499
+ _debug_print(html)
251
500
  rexp = %r`"https://ajax\.googleapis\.com/ajax/libs/#{library}/`
252
501
  site_url = nil
253
502
  versions = []
@@ -258,9 +507,7 @@ module CDNGet
258
507
  found = true
259
508
  if text =~ /<dt>.*?snippet:<\/dt>\s*<dd>(.*?)<\/dd>/m
260
509
  s = $1
261
- s.scan(/\b(?:src|href)="([^"]*?)"/) do |href,|
262
- urls << href
263
- end
510
+ s.scan(/\b(?:src|href)="([^"]*?)"/) {|href,| urls << href }
264
511
  end
265
512
  if text =~ /<dt>site:<\/dt>\s*<dd>(.*?)<\/dd>/m
266
513
  s = $1
@@ -281,6 +528,7 @@ module CDNGet
281
528
  return {
282
529
  name: library,
283
530
  site: site_url,
531
+ info: "#{SITE_URL}\##{library}",
284
532
  urls: urls,
285
533
  versions: versions,
286
534
  }
@@ -301,6 +549,7 @@ module CDNGet
301
549
  return {
302
550
  name: d[:name],
303
551
  site: d[:site],
552
+ info: "#{SITE_URL}\##{library}",
304
553
  urls: urls,
305
554
  files: files,
306
555
  baseurl: baseurl,
@@ -333,24 +582,28 @@ module CDNGet
333
582
  @script = script || File.basename($0)
334
583
  end
335
584
 
336
- def help_message
585
+ def help_message()
337
586
  script = @script
338
587
  return <<END
339
- #{script} -- download files from public CDN
588
+ #{script} -- download files from public CDN (cdnjs/jsdelivr/unpkg/google)
340
589
 
341
- Usage: #{script} [options] [CDN] [library] [version] [directory]
590
+ Usage: #{script} [<options>] [<CDN> [<library> [<version> [<directory>]]]]
342
591
 
343
592
  Options:
344
593
  -h, --help : help
345
594
  -v, --version : version
346
595
  -q, --quiet : minimal output
596
+ --debug : (debug mode)
347
597
 
348
598
  Example:
349
- $ #{script} # list public CDN
599
+ $ #{script} # list public CDN names
350
600
  $ #{script} [-q] cdnjs # list libraries
601
+ $ #{script} [-q] cdnjs 'jquery*' # search libraries
351
602
  $ #{script} [-q] cdnjs jquery # list versions
352
- $ #{script} [-q] cdnjs jquery 2.2.0 # list files
353
- $ #{script} [-q] cdnjs jquery 2.2.0 /tmp # download files
603
+ $ #{script} [-q] cdnjs jquery latest # show latest version
604
+ $ #{script} [-q] cdnjs jquery 3.6.0 # list files
605
+ $ #{script} [-q] cdnjs jquery 3.6.0 /tmp # download files into directory
606
+
354
607
  END
355
608
  end
356
609
 
@@ -365,10 +618,11 @@ END
365
618
  end
366
619
 
367
620
  def run(*args)
368
- cmdopts = parse_cmdopts(args, "hvq", ["help", "version", "quiet"])
621
+ cmdopts = parse_cmdopts(args, "hvq", ["help", "version", "quiet", "debug"])
369
622
  return help_message() if cmdopts['h'] || cmdopts['help']
370
623
  return RELEASE + "\n" if cmdopts['v'] || cmdopts['version']
371
624
  @quiet = cmdopts['quiet'] || cmdopts['q']
625
+ @debug_mode = cmdopts['debug']
372
626
  #
373
627
  validate(args[1], args[2])
374
628
  #
@@ -380,11 +634,9 @@ END
380
634
  return do_list_libraries(cdn_code)
381
635
  when 2
382
636
  cdn_code, library = args
383
- if library.include?('*')
384
- return do_search_libraries(cdn_code, library)
385
- else
386
- return do_find_library(cdn_code, library)
387
- end
637
+ return library.include?('*') \
638
+ ? do_search_libraries(cdn_code, library) \
639
+ : do_find_library(cdn_code, library)
388
640
  when 3
389
641
  cdn_code, library, version = args
390
642
  return do_get_library(cdn_code, library, version)
@@ -438,35 +690,29 @@ END
438
690
  def find_cdn(cdn_code)
439
691
  klass = CLASSES.find {|c| c::CODE == cdn_code } or
440
692
  raise CommandError.new("#{cdn_code}: no such CDN.")
441
- return klass.new
693
+ return klass.new(debug_mode: @debug_mode)
442
694
  end
443
695
 
444
696
  def render_list(list)
445
- if @quiet
446
- return list.collect {|d| "#{d[:name]}\n" }.join()
447
- else
448
- return list.collect {|d| "%-20s # %s\n" % [d[:name], d[:desc]] }.join()
449
- end
697
+ return list.collect {|d| "#{d[:name]}\n" }.join() if @quiet
698
+ return list.collect {|d| "%-20s # %s\n" % [d[:name], d[:desc]] }.join()
450
699
  end
451
700
 
452
- def do_list_cdns
453
- if @quiet
454
- return CLASSES.map {|c| "#{c::CODE}\n" }.join()
455
- else
456
- return CLASSES.map {|c| "%-10s # %s\n" % [c::CODE, c::SITE_URL] }.join()
457
- end
701
+ def do_list_cdns()
702
+ return CLASSES.map {|c| "#{c::CODE}\n" }.join() if @quiet
703
+ return CLASSES.map {|c| "%-10s # %s\n" % [c::CODE, c::SITE_URL] }.join()
458
704
  end
459
705
 
460
706
  def do_list_libraries(cdn_code)
461
707
  cdn = find_cdn(cdn_code)
462
- return render_list(cdn.list)
708
+ list = cdn.list() or
709
+ raise CommandError.new("#{cdn_code}: cannot list libraries; please specify pattern such as 'jquery*'.")
710
+ return render_list(list)
463
711
  end
464
712
 
465
713
  def do_search_libraries(cdn_code, pattern)
466
714
  cdn = find_cdn(cdn_code)
467
- rexp_str = pattern.split('*', -1).collect {|x| Regexp.escape(x) }.join('.*')
468
- rexp = Regexp.compile("\\A#{rexp_str}\\z", Regexp::IGNORECASE)
469
- return render_list(cdn.list.select {|a| a[:name] =~ rexp })
715
+ return render_list(cdn.search(pattern))
470
716
  end
471
717
 
472
718
  def do_find_library(cdn_code, library)
@@ -478,10 +724,12 @@ END
478
724
  s << "#{ver}\n"
479
725
  end if d[:versions]
480
726
  else
481
- s << "name: #{d[:name]}\n"
482
- s << "desc: #{d[:desc]}\n" if d[:desc]
483
- s << "tags: #{d[:tags]}\n" if d[:tags]
484
- s << "site: #{d[:site]}\n" if d[:site]
727
+ s << "name: #{d[:name]}\n"
728
+ s << "desc: #{d[:desc]}\n" if d[:desc]
729
+ s << "tags: #{d[:tags]}\n" if d[:tags]
730
+ s << "site: #{d[:site]}\n" if d[:site]
731
+ s << "info: #{d[:info]}\n" if d[:info]
732
+ s << "license: #{d[:license]}\n" if d[:license]
485
733
  s << "snippet: |\n" << d[:snippet].gsub(/^/, ' ') if d[:snippet]
486
734
  s << "versions:\n"
487
735
  d[:versions].each do |ver|
@@ -493,6 +741,7 @@ END
493
741
 
494
742
  def do_get_library(cdn_code, library, version)
495
743
  cdn = find_cdn(cdn_code)
744
+ version = _latest_version(cdn, library) if version == 'latest'
496
745
  d = cdn.get(library, version)
497
746
  s = ""
498
747
  if @quiet
@@ -502,8 +751,13 @@ END
502
751
  else
503
752
  s << "name: #{d[:name]}\n"
504
753
  s << "version: #{d[:version]}\n"
505
- s << "tags: #{d[:tags]}\n" if d[:tags]
506
- s << "site: #{d[:site]}\n" if d[:site]
754
+ s << "desc: #{d[:desc]}\n" if d[:desc]
755
+ s << "tags: #{d[:tags]}\n" if d[:tags]
756
+ s << "site: #{d[:site]}\n" if d[:site]
757
+ s << "info: #{d[:info]}\n" if d[:info]
758
+ s << "npmpkg: #{d[:npmpkg]}\n" if d[:npmpkg]
759
+ s << "default: #{d[:default]}\n" if d[:default]
760
+ s << "license: #{d[:license]}\n" if d[:license]
507
761
  s << "snippet: |\n" << d[:snippet].gsub(/^/, ' ') if d[:snippet]
508
762
  s << "urls:\n" if d[:urls]
509
763
  d[:urls].each do |url|
@@ -515,10 +769,18 @@ END
515
769
 
516
770
  def do_download_library(cdn_code, library, version, basedir)
517
771
  cdn = find_cdn(cdn_code)
772
+ version = _latest_version(cdn, library) if version == 'latest'
518
773
  cdn.download(library, version, basedir, quiet: @quiet)
519
774
  return nil
520
775
  end
521
776
 
777
+ private
778
+
779
+ def _latest_version(cdn, library)
780
+ d = cdn.find(library)
781
+ return d[:versions].first
782
+ end
783
+
522
784
  end
523
785
 
524
786