cdnget 0.2.0 → 1.0.1

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.
data/lib/cdnget.rb 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.2.0 $'.split()[1]
33
+ RELEASE = '$Release: 1.0.1 $'.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,36 @@ 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
140
+ skipfile = d[:skipfile] # ex: /\.DS_Store\z/
58
141
  d[:files].each do |file|
59
142
  filepath = File.join(target_dir, file)
143
+ #
144
+ if skipfile && file =~ skipfile
145
+ puts "#{filepath} ... Skipped" # for example, skip '.DS_Store' files
146
+ next
147
+ end
148
+ #
149
+ if filepath.end_with?('/')
150
+ if File.exist?(filepath)
151
+ puts "#{filepath} ... Done (Already exists)" unless quiet
152
+ else
153
+ print "#{filepath} ..." unless quiet
154
+ FileUtils.mkdir_p(filepath)
155
+ puts " Done (Created)" unless quiet
156
+ end
157
+ next
158
+ end
159
+ #
60
160
  dirpath = File.dirname(filepath)
61
161
  print "#{filepath} ..." unless quiet
62
162
  url = File.join(d[:baseurl], file) # not use URI.join!
63
- content = fetch(url)
163
+ uri = URI.parse(url)
164
+ http ||= HttpConnection.new(uri)
165
+ content = http.get(uri)
64
166
  content = content.force_encoding('ascii-8bit')
65
167
  print " Done (#{format_integer(content.bytesize)} byte)" unless quiet
66
168
  FileUtils.mkdir_p(dirpath) unless File.exist?(dirpath)
@@ -72,17 +174,32 @@ module CDNGet
72
174
  end
73
175
  puts() unless quiet
74
176
  end
177
+ http.close() if http
75
178
  nil
76
179
  end
77
180
 
78
181
  protected
79
182
 
183
+ def http_get(url)
184
+ ## * `open()` on Ruby 3.X can't open http url
185
+ ## * `URI.open()` on Ruby <= 2.4 raises NoMethodError (private method `open' called)
186
+ ## * `URI.__send__(:open)` is a hack to work on both Ruby 2.X and 3.X
187
+ return URI.__send__(:open, url, 'rb') {|f| f.read() }
188
+ end
189
+
80
190
  def fetch(url, library=nil)
81
191
  begin
82
- html = open(url, 'rb') {|f| f.read() }
192
+ html = http_get(url)
83
193
  return html
84
- rescue OpenURI::HTTPError => ex
85
- raise CommandError.new("GET #{url} : #{ex.message}")
194
+ rescue OpenURI::HTTPError => exc
195
+ if ! (exc.message == "404 Not Found" && library)
196
+ raise CommandError.new("GET #{url} : #{exc.message}")
197
+ elsif ! library.end_with?('js')
198
+ raise CommandError.new("#{library}: Library not found.")
199
+ else
200
+ maybe = library.end_with?('.js') ? library.sub('.js', 'js') : library.sub(/js$/, '.js')
201
+ raise CommandError.new("#{library}: Library not found (maybe '#{maybe}'?).")
202
+ end
86
203
  end
87
204
  end
88
205
 
@@ -101,83 +218,80 @@ module CDNGet
101
218
  return value.to_s.reverse.scan(/..?.?/).collect {|s| s.reverse }.reverse.join(',')
102
219
  end
103
220
 
104
- end
105
-
221
+ def _debug_print(x)
222
+ if @debug_mode
223
+ $stderr.puts "\e[0;35m*** #{PP.pp(x,'')}\e[0m"
224
+ end
225
+ end
106
226
 
107
- class HttpError < StandardError
108
227
  end
109
228
 
110
229
 
111
- class CDNJS < Base # TODO: use jsdelivr api
230
+ class CDNJS < Base
112
231
  CODE = "cdnjs"
113
232
  SITE_URL = 'https://cdnjs.com/'
114
233
 
115
234
  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()}'?).")
235
+ json_str = super
236
+ if json_str == "{}" && library
237
+ if library.end_with?('js')
238
+ maybe = library.end_with?('.js') \
239
+ ? library.sub('.js', 'js') \
240
+ : library.sub(/js$/, '.js')
241
+ raise CommandError.new("#{library}: Library not found (maybe '#{maybe}'?).")
242
+ else
243
+ raise CommandError.new("#{library}: Library not found.")
123
244
  end
124
- return html
125
- rescue OpenURI::HTTPError => ex
126
- raise HttpError.new("GET #{url} : #{ex.message}")
127
245
  end
246
+ return json_str
128
247
  end
129
248
  protected :fetch
130
249
 
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
250
+ def list()
251
+ jstr = fetch("https://api.cdnjs.com/libraries?fields=name,description")
252
+ jdata = JSON.parse(jstr)
253
+ _debug_print(jdata)
254
+ libs = jdata['results'].collect {|d| {name: d['name'], desc: d['description']} }
140
255
  return libs.sort_by {|d| d[:name] }.uniq
141
256
  end
142
257
 
143
258
  def find(library)
144
259
  validate(library, nil)
145
- html = fetch("https://cdnjs.com/libraries/#{library}", library)
146
- flagment = html.split(/<select class=".*?version-selector.*?"/, 2).last
147
- flagment = flagment.split(/<\/select>/, 2).first
148
- versions = []
149
- flagment.scan(/<option value="([^"]+)" *(?:selected)?>/) do |ver,|
150
- versions << ver
151
- end
152
- desc = tags = nil
153
- if html =~ /<\/p>\s*<p>(.*?)<\/p>\s*<em>(.*?)<\/em>/
154
- desc = $1
155
- tags = $2
156
- end
260
+ jstr = fetch("https://api.cdnjs.com/libraries/#{library}", library)
261
+ jdata = JSON.parse(jstr)
262
+ _debug_print(jdata)
263
+ versions = jdata['assets'].collect {|d| d['version'] }\
264
+ .sort_by {|v| v.split(/[-.]/).map(&:to_i) }
157
265
  return {
158
266
  name: library,
159
- desc: desc,
160
- tags: tags,
161
- versions: versions,
267
+ desc: jdata['description'],
268
+ tags: (jdata['keywords'] || []).join(", "),
269
+ site: jdata['homepage'],
270
+ info: File.join(SITE_URL, "/libraries/#{library}"),
271
+ license: jdata['license'],
272
+ versions: versions.reverse(),
162
273
  }
163
274
  end
164
275
 
165
276
  def get(library, version)
166
277
  validate(library, version)
167
- html = fetch("https://cdnjs.com/libraries/#{library}/#{version}", library)
278
+ jstr = fetch("https://api.cdnjs.com/libraries/#{library}", library)
279
+ jdata = JSON.parse(jstr)
280
+ _debug_print(jdata)
281
+ d = jdata['assets'].find {|d| d['version'] == version } or
282
+ raise CommandError.new("#{library}/#{version}: Library or version not found.")
168
283
  baseurl = "https://cdnjs.cloudflare.com/ajax/libs/#{library}/#{version}/"
169
- basepat = Regexp.escape("#{library}/#{version}")
170
- files = []
171
- html.scan(%r`>#{basepat}/([^<]+)<\/p>`) do |file,|
172
- files << file.gsub(/&#x2F;/, '/')
173
- end
174
- urls = files.collect {|s| baseurl + s }
175
284
  return {
176
285
  name: library,
177
286
  version: version,
178
- urls: urls,
179
- files: files,
287
+ desc: jdata['description'],
288
+ tags: (jdata['keywords'] || []).join(", "),
289
+ site: jdata['homepage'],
290
+ info: File.join(SITE_URL, "/libraries/#{library}/#{version}"),
291
+ urls: d['files'].collect {|s| baseurl + s },
292
+ files: d['files'],
180
293
  baseurl: baseurl,
294
+ license: jdata['license'],
181
295
  }
182
296
  end
183
297
 
@@ -187,59 +301,201 @@ module CDNGet
187
301
  class JSDelivr < Base
188
302
  CODE = "jsdelivr"
189
303
  SITE_URL = "https://www.jsdelivr.com/"
190
- API_URL = "https://api.jsdelivr.com/v1/jsdelivr/libraries"
304
+ #API_URL = "https://api.jsdelivr.com/v1/jsdelivr/libraries"
305
+ API_URL = "https://data.jsdelivr.com/v1"
306
+ HEADERS = {
307
+ "x-algo""lia-app""lication-id"=>"OFCNC""OG2CU",
308
+ "x-algo""lia-api""-key"=>"f54e21fa3a2""a0160595bb05""8179bfb1e",
309
+ }
191
310
 
192
- def list
193
- json = fetch("#{API_URL}?fields=name,description,homepage")
194
- arr = JSON.load(json)
195
- return arr.collect {|d|
196
- {name: d["name"], desc: d["description"], site: d["homepage"] }
311
+ def list()
312
+ return nil # nil means that this CDN can't list libraries without pattern
313
+ end
314
+
315
+ def search(pattern)
316
+ form_data = {
317
+ query: pattern,
318
+ page: "0",
319
+ hitsPerPage: "1000",
320
+ attributesToHighlight: '[]',
321
+ attributesToRetrieve: '["name","description","version"]'
322
+ }
323
+ payload = JSON.dump({"params"=>URI.encode_www_form(form_data)})
324
+ url = "https://ofcncog2cu-3.algolianet.com/1/indexes/npm-search/query"
325
+ uri = URI.parse(url)
326
+ json = HttpConnection.open(uri, HEADERS) {|http| http.post(uri, payload) }
327
+ jdata = JSON.load(json)
328
+ _debug_print(jdata)
329
+ return jdata["hits"].select {|d|
330
+ File.fnmatch(pattern, d["name"], File::FNM_CASEFOLD)
331
+ }.collect {|d|
332
+ {name: d["name"], desc: d["description"], version: d["version"]}
197
333
  }
198
334
  end
199
335
 
200
336
  def find(library)
201
337
  validate(library, nil)
202
- json = fetch("#{API_URL}?name=#{library}&fields=name,description,homepage,versions")
203
- arr = JSON.load(json)
204
- d = arr.first or
205
- raise CommandError.new("#{library}: Library not found.")
338
+ url = "https://ofcncog2cu-dsn.algolia.net/1/indexes/npm-search/#{library}"
339
+ uri = URI.parse(url)
340
+ begin
341
+ json = HttpConnection.open(uri, HEADERS) {|http| http.get(uri) }
342
+ rescue HttpError
343
+ raise CommandError, "#{library}: Library not found."
344
+ end
345
+ dict1 = JSON.load(json)
346
+ _debug_print(dict1)
347
+ #
348
+ json = fetch("#{API_URL}/package/npm/#{library}")
349
+ dict2 = JSON.load(json)
350
+ _debug_print(dict2)
351
+ #
352
+ d = dict1
206
353
  return {
207
- name: d['name'],
208
- desc: d['description'],
209
- site: d['homepage'],
210
- versions: d['versions'],
354
+ name: d['name'],
355
+ desc: d['description'],
356
+ #versions: d['versions'].collect {|k,v| k },
357
+ versions: dict2['versions'],
358
+ tags: (d['keywords'] || []).join(", "),
359
+ site: d['homepage'],
360
+ info: File.join(SITE_URL, "/package/npm/#{library}"),
361
+ license: d['license'],
211
362
  }
212
363
  end
213
364
 
214
365
  def get(library, version)
215
366
  validate(library, version)
216
- baseurl = "https://cdn.jsdelivr.net/#{library}/#{version}"
217
- url = "#{API_URL}/#{library}/#{version}"
218
- json = fetch("#{API_URL}/#{library}/#{version}")
219
- files = JSON.load(json)
220
- ! files.empty? or
221
- raise CommandError.new("#{library}: Library not found.")
222
- urls = files.collect {|x| "#{baseurl}/#{x}" }
367
+ url = File.join(API_URL, "/package/npm/#{library}@#{version}/flat")
368
+ begin
369
+ json = fetch(url, library)
370
+ rescue CommandError
371
+ raise CommandError.new("#{library}@#{version}: Library or version not found.")
372
+ end
373
+ jdata = JSON.load(json)
374
+ files = jdata["files"].collect {|d| d["name"] }
375
+ baseurl = "https://cdn.jsdelivr.net/npm/#{library}@#{version}"
376
+ _debug_print(jdata)
377
+ #
378
+ dict = find(library)
379
+ dict.delete(:versions)
380
+ dict.update({
381
+ version: version,
382
+ info: File.join(SITE_URL, "/package/npm/#{library}?version=#{version}"),
383
+ npmpkg: "https://registry.npmjs.org/#{library}/-/#{library}-#{version}.tgz",
384
+ urls: files.collect {|x| baseurl + x },
385
+ files: files,
386
+ baseurl: baseurl,
387
+ default: jdata["default"],
388
+ destdir: "#{library}@#{version}",
389
+ })
390
+ return dict
391
+ end
392
+
393
+ end
394
+
395
+
396
+ class Unpkg < Base
397
+ CODE = "unpkg"
398
+ SITE_URL = "https://unpkg.com/"
399
+ #API_URL = "https://www.npmjs.com"
400
+ API_URL = "https://api.npms.io/v2"
401
+
402
+ protected
403
+
404
+ def http_get(url)
405
+ return URI.__send__(:open, url, 'rb', {"x-spiferack"=>"1"}) {|f| f.read() }
406
+ end
407
+
408
+ public
409
+
410
+ def list()
411
+ return nil # nil means that this CDN can't list libraries without pattern
412
+ end
413
+
414
+ def search(pattern)
415
+ #json = fetch("#{API_URL}/search?q=#{pattern}")
416
+ json = fetch("#{API_URL}/search?q=#{pattern}&size=250")
417
+ jdata = JSON.load(json)
418
+ _debug_print(jdata)
419
+ #arr = jdata["objects"] # www.npmjs.com
420
+ arr = jdata["results"] # api.npms.io
421
+ return arr.select {|dict|
422
+ File.fnmatch(pattern, dict["package"]["name"], File::FNM_CASEFOLD)
423
+ }.collect {|dict|
424
+ d = dict["package"]
425
+ {name: d["name"], desc: d["description"], version: d["version"]}
426
+ }
427
+ end
428
+
429
+ def find(library)
430
+ validate(library, nil)
431
+ json = fetch("#{API_URL}/package/#{library}", library)
432
+ jdata = JSON.load(json)
433
+ _debug_print(jdata)
434
+ dict = jdata["collected"]["metadata"]
435
+ versions = [dict["version"]]
436
+ #
437
+ url = File.join(SITE_URL, "/browse/#{library}/")
438
+ html = fetch(url, library)
439
+ _debug_print(html)
440
+ if html =~ /<script>window.__DATA__\s*=\s*(.*?)<\/script>/m
441
+ jdata2 = JSON.load($1)
442
+ versions = jdata2["availableVersions"].reverse()
443
+ end
444
+ #
223
445
  return {
446
+ name: dict["name"],
447
+ desc: dict["description"],
448
+ tags: (dict["keywords"] || []).join(", "),
449
+ site: dict["links"] ? dict["links"]["homepage"] : dict["links"]["npm"],
450
+ info: File.join(SITE_URL, "/browse/#{library}/"),
451
+ versions: versions,
452
+ license: dict["license"],
453
+ }
454
+ end
455
+
456
+ def get(library, version)
457
+ validate(library, version)
458
+ dict = find(library)
459
+ dict.delete(:versions)
460
+ #
461
+ url = "https://data.jsdelivr.com/v1/package/npm/#{library}@#{version}/flat"
462
+ begin
463
+ json = fetch(url, library)
464
+ rescue CommandError
465
+ raise CommandError.new("#{library}@#{version}: Library or version not found.")
466
+ end
467
+ jdata = JSON.load(json)
468
+ files = jdata["files"].collect {|d| d["name"] }
469
+ baseurl = File.join(SITE_URL, "/#{library}@#{version}")
470
+ _debug_print(jdata)
471
+ #
472
+ dict.update({
224
473
  name: library,
225
474
  version: version,
226
- urls: urls,
475
+ info: File.join(SITE_URL, "/browse/#{library}@#{version}/"),
476
+ npmpkg: "https://registry.npmjs.org/#{library}/-/#{library}-#{version}.tgz",
477
+ urls: files.collect {|x| baseurl+x },
227
478
  files: files,
228
479
  baseurl: baseurl,
229
- }
480
+ default: jdata["default"],
481
+ destdir: "#{library}@#{version}",
482
+ skipfile: /\.DS_Store\z/, # downloading '.DS_Store' from UNPKG results in 403
483
+ })
484
+ return dict
230
485
  end
231
486
 
232
487
  end
233
488
 
234
489
 
235
- class GoogleCDN < Base # TODO: use jsdelivr api
490
+ class GoogleCDN < Base
236
491
  CODE = "google"
237
492
  SITE_URL = 'https://developers.google.com/speed/libraries/'
238
493
 
239
- def list
240
- libs = []
494
+ def list()
241
495
  html = fetch("https://developers.google.com/speed/libraries/")
496
+ _debug_print(html)
242
497
  rexp = %r`"https://ajax\.googleapis\.com/ajax/libs/([^/]+)/([^/]+)/([^"]+)"`
498
+ libs = []
243
499
  html.scan(rexp) do |lib, ver, file|
244
500
  libs << {name: lib, desc: "latest version: #{ver}" }
245
501
  end
@@ -249,6 +505,7 @@ module CDNGet
249
505
  def find(library)
250
506
  validate(library, nil)
251
507
  html = fetch("https://developers.google.com/speed/libraries/")
508
+ _debug_print(html)
252
509
  rexp = %r`"https://ajax\.googleapis\.com/ajax/libs/#{library}/`
253
510
  site_url = nil
254
511
  versions = []
@@ -259,9 +516,7 @@ module CDNGet
259
516
  found = true
260
517
  if text =~ /<dt>.*?snippet:<\/dt>\s*<dd>(.*?)<\/dd>/m
261
518
  s = $1
262
- s.scan(/\b(?:src|href)="([^"]*?)"/) do |href,|
263
- urls << href
264
- end
519
+ s.scan(/\b(?:src|href)="([^"]*?)"/) {|href,| urls << href }
265
520
  end
266
521
  if text =~ /<dt>site:<\/dt>\s*<dd>(.*?)<\/dd>/m
267
522
  s = $1
@@ -282,6 +537,7 @@ module CDNGet
282
537
  return {
283
538
  name: library,
284
539
  site: site_url,
540
+ info: "#{SITE_URL}\##{library}",
285
541
  urls: urls,
286
542
  versions: versions,
287
543
  }
@@ -302,6 +558,7 @@ module CDNGet
302
558
  return {
303
559
  name: d[:name],
304
560
  site: d[:site],
561
+ info: "#{SITE_URL}\##{library}",
305
562
  urls: urls,
306
563
  files: files,
307
564
  baseurl: baseurl,
@@ -334,24 +591,28 @@ module CDNGet
334
591
  @script = script || File.basename($0)
335
592
  end
336
593
 
337
- def help_message
594
+ def help_message()
338
595
  script = @script
339
596
  return <<END
340
- #{script} -- download files from public CDN
597
+ #{script} -- download files from public CDN (cdnjs/jsdelivr/unpkg/google)
341
598
 
342
- Usage: #{script} [options] [CDN] [library] [version] [directory]
599
+ Usage: #{script} [<options>] [<CDN> [<library> [<version> [<directory>]]]]
343
600
 
344
601
  Options:
345
602
  -h, --help : help
346
603
  -v, --version : version
347
604
  -q, --quiet : minimal output
605
+ --debug : (debug mode)
348
606
 
349
607
  Example:
350
- $ #{script} # list public CDN
608
+ $ #{script} # list public CDN names
351
609
  $ #{script} [-q] cdnjs # list libraries
610
+ $ #{script} [-q] cdnjs 'jquery*' # search libraries
352
611
  $ #{script} [-q] cdnjs jquery # list versions
353
- $ #{script} [-q] cdnjs jquery 2.2.0 # list files
354
- $ #{script} [-q] cdnjs jquery 2.2.0 /tmp # download files
612
+ $ #{script} [-q] cdnjs jquery latest # show latest version
613
+ $ #{script} [-q] cdnjs jquery 3.6.0 # list files
614
+ $ #{script} [-q] cdnjs jquery 3.6.0 /tmp # download files into directory
615
+
355
616
  END
356
617
  end
357
618
 
@@ -366,10 +627,11 @@ END
366
627
  end
367
628
 
368
629
  def run(*args)
369
- cmdopts = parse_cmdopts(args, "hvq", ["help", "version", "quiet"])
630
+ cmdopts = parse_cmdopts(args, "hvq", ["help", "version", "quiet", "debug"])
370
631
  return help_message() if cmdopts['h'] || cmdopts['help']
371
632
  return RELEASE + "\n" if cmdopts['v'] || cmdopts['version']
372
633
  @quiet = cmdopts['quiet'] || cmdopts['q']
634
+ @debug_mode = cmdopts['debug']
373
635
  #
374
636
  validate(args[1], args[2])
375
637
  #
@@ -381,11 +643,9 @@ END
381
643
  return do_list_libraries(cdn_code)
382
644
  when 2
383
645
  cdn_code, library = args
384
- if library.include?('*')
385
- return do_search_libraries(cdn_code, library)
386
- else
387
- return do_find_library(cdn_code, library)
388
- end
646
+ return library.include?('*') \
647
+ ? do_search_libraries(cdn_code, library) \
648
+ : do_find_library(cdn_code, library)
389
649
  when 3
390
650
  cdn_code, library, version = args
391
651
  return do_get_library(cdn_code, library, version)
@@ -439,35 +699,29 @@ END
439
699
  def find_cdn(cdn_code)
440
700
  klass = CLASSES.find {|c| c::CODE == cdn_code } or
441
701
  raise CommandError.new("#{cdn_code}: no such CDN.")
442
- return klass.new
702
+ return klass.new(debug_mode: @debug_mode)
443
703
  end
444
704
 
445
705
  def render_list(list)
446
- if @quiet
447
- return list.collect {|d| "#{d[:name]}\n" }.join()
448
- else
449
- return list.collect {|d| "%-20s # %s\n" % [d[:name], d[:desc]] }.join()
450
- end
706
+ return list.collect {|d| "#{d[:name]}\n" }.join() if @quiet
707
+ return list.collect {|d| "%-20s # %s\n" % [d[:name], d[:desc]] }.join()
451
708
  end
452
709
 
453
- def do_list_cdns
454
- if @quiet
455
- return CLASSES.map {|c| "#{c::CODE}\n" }.join()
456
- else
457
- return CLASSES.map {|c| "%-10s # %s\n" % [c::CODE, c::SITE_URL] }.join()
458
- end
710
+ def do_list_cdns()
711
+ return CLASSES.map {|c| "#{c::CODE}\n" }.join() if @quiet
712
+ return CLASSES.map {|c| "%-10s # %s\n" % [c::CODE, c::SITE_URL] }.join()
459
713
  end
460
714
 
461
715
  def do_list_libraries(cdn_code)
462
716
  cdn = find_cdn(cdn_code)
463
- return render_list(cdn.list)
717
+ list = cdn.list() or
718
+ raise CommandError.new("#{cdn_code}: cannot list libraries; please specify pattern such as 'jquery*'.")
719
+ return render_list(list)
464
720
  end
465
721
 
466
722
  def do_search_libraries(cdn_code, pattern)
467
723
  cdn = find_cdn(cdn_code)
468
- rexp_str = pattern.split('*', -1).collect {|x| Regexp.escape(x) }.join('.*')
469
- rexp = Regexp.compile("\\A#{rexp_str}\\z", Regexp::IGNORECASE)
470
- return render_list(cdn.list.select {|a| a[:name] =~ rexp })
724
+ return render_list(cdn.search(pattern))
471
725
  end
472
726
 
473
727
  def do_find_library(cdn_code, library)
@@ -479,10 +733,12 @@ END
479
733
  s << "#{ver}\n"
480
734
  end if d[:versions]
481
735
  else
482
- s << "name: #{d[:name]}\n"
483
- s << "desc: #{d[:desc]}\n" if d[:desc]
484
- s << "tags: #{d[:tags]}\n" if d[:tags]
485
- s << "site: #{d[:site]}\n" if d[:site]
736
+ s << "name: #{d[:name]}\n"
737
+ s << "desc: #{d[:desc]}\n" if d[:desc]
738
+ s << "tags: #{d[:tags]}\n" if d[:tags]
739
+ s << "site: #{d[:site]}\n" if d[:site]
740
+ s << "info: #{d[:info]}\n" if d[:info]
741
+ s << "license: #{d[:license]}\n" if d[:license]
486
742
  s << "snippet: |\n" << d[:snippet].gsub(/^/, ' ') if d[:snippet]
487
743
  s << "versions:\n"
488
744
  d[:versions].each do |ver|
@@ -494,6 +750,7 @@ END
494
750
 
495
751
  def do_get_library(cdn_code, library, version)
496
752
  cdn = find_cdn(cdn_code)
753
+ version = _latest_version(cdn, library) if version == 'latest'
497
754
  d = cdn.get(library, version)
498
755
  s = ""
499
756
  if @quiet
@@ -503,8 +760,13 @@ END
503
760
  else
504
761
  s << "name: #{d[:name]}\n"
505
762
  s << "version: #{d[:version]}\n"
506
- s << "tags: #{d[:tags]}\n" if d[:tags]
507
- s << "site: #{d[:site]}\n" if d[:site]
763
+ s << "desc: #{d[:desc]}\n" if d[:desc]
764
+ s << "tags: #{d[:tags]}\n" if d[:tags]
765
+ s << "site: #{d[:site]}\n" if d[:site]
766
+ s << "info: #{d[:info]}\n" if d[:info]
767
+ s << "npmpkg: #{d[:npmpkg]}\n" if d[:npmpkg]
768
+ s << "default: #{d[:default]}\n" if d[:default]
769
+ s << "license: #{d[:license]}\n" if d[:license]
508
770
  s << "snippet: |\n" << d[:snippet].gsub(/^/, ' ') if d[:snippet]
509
771
  s << "urls:\n" if d[:urls]
510
772
  d[:urls].each do |url|
@@ -516,10 +778,18 @@ END
516
778
 
517
779
  def do_download_library(cdn_code, library, version, basedir)
518
780
  cdn = find_cdn(cdn_code)
781
+ version = _latest_version(cdn, library) if version == 'latest'
519
782
  cdn.download(library, version, basedir, quiet: @quiet)
520
783
  return nil
521
784
  end
522
785
 
786
+ private
787
+
788
+ def _latest_version(cdn, library)
789
+ d = cdn.find(library)
790
+ return d[:versions].first
791
+ end
792
+
523
793
  end
524
794
 
525
795