cdnget 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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