cdnget 0.3.1 → 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.
- checksums.yaml +5 -5
- data/CHANGES.md +15 -0
- data/README.md +6 -4
- data/Rakefile +1 -1
- data/bin/cdnget +354 -90
- data/cdnget.gemspec +3 -3
- data/lib/cdnget.rb +354 -90
- data/test/cdnget_test.rb +634 -108
- metadata +9 -8
    
        data/lib/cdnget.rb
    CHANGED
    
    | @@ -2,30 +2,97 @@ | |
| 2 2 | 
             
            # -*- coding: utf-8 -*-
         | 
| 3 3 |  | 
| 4 4 | 
             
            ##
         | 
| 5 | 
            -
            ## Download files from CDN (CDNJS,  | 
| 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  | 
| 16 | 
            -
            ##  $ cdnget [-q] cdnjs jquery  | 
| 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'
         | 
| 20 22 | 
             
            require 'uri'
         | 
| 23 | 
            +
            require 'net/http'
         | 
| 24 | 
            +
            require 'openssl'
         | 
| 21 25 | 
             
            require 'json'
         | 
| 22 26 | 
             
            require 'fileutils'
         | 
| 27 | 
            +
            require 'pp'
         | 
| 23 28 |  | 
| 24 29 |  | 
| 25 30 | 
             
            module CDNGet
         | 
| 26 31 |  | 
| 27 32 |  | 
| 28 | 
            -
              RELEASE = '$Release: 0. | 
| 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 | 
            +
             | 
| 29 96 |  | 
| 30 97 | 
             
              CLASSES = []
         | 
| 31 98 |  | 
| @@ -36,10 +103,22 @@ module CDNGet | |
| 36 103 | 
             
                  CLASSES << klass
         | 
| 37 104 | 
             
                end
         | 
| 38 105 |  | 
| 106 | 
            +
                def initialize(debug_mode: false)
         | 
| 107 | 
            +
                  @debug_mode = debug_mode
         | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
                attr_reader :debug_mode
         | 
| 110 | 
            +
             | 
| 39 111 | 
             
                def list()
         | 
| 40 112 | 
             
                  raise NotImplementedError.new("#{self.class.name}#list(): not implemented yet.")
         | 
| 41 113 | 
             
                end
         | 
| 42 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 | 
            +
             | 
| 43 122 | 
             
                def find(library)
         | 
| 44 123 | 
             
                  raise NotImplementedError.new("#{self.class.name}#find(): not implemented yet.")
         | 
| 45 124 | 
             
                end
         | 
| @@ -54,14 +133,28 @@ module CDNGet | |
| 54 133 | 
             
                    raise CommandError.new("#{basedir}: not exist.")
         | 
| 55 134 | 
             
                  File.directory?(basedir)  or
         | 
| 56 135 | 
             
                    raise CommandError.new("#{basedir}: not a directory.")
         | 
| 57 | 
            -
                  target_dir = File.join(basedir, library, version)
         | 
| 58 136 | 
             
                  d = get(library, version)
         | 
| 137 | 
            +
                  target_dir = d[:destdir] ? File.join(basedir, d[:destdir]) \
         | 
| 138 | 
            +
                                           : File.join(basedir, library, version)
         | 
| 139 | 
            +
                  http = nil
         | 
| 59 140 | 
             
                  d[:files].each do |file|
         | 
| 60 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
         | 
| 61 152 | 
             
                    dirpath  = File.dirname(filepath)
         | 
| 62 153 | 
             
                    print "#{filepath} ..." unless quiet
         | 
| 63 154 | 
             
                    url = File.join(d[:baseurl], file)   # not use URI.join!
         | 
| 64 | 
            -
                     | 
| 155 | 
            +
                    uri = URI.parse(url)
         | 
| 156 | 
            +
                    http ||= HttpConnection.new(uri)
         | 
| 157 | 
            +
                    content = http.get(uri)
         | 
| 65 158 | 
             
                    content = content.force_encoding('ascii-8bit')
         | 
| 66 159 | 
             
                    print " Done (#{format_integer(content.bytesize)} byte)" unless quiet
         | 
| 67 160 | 
             
                    FileUtils.mkdir_p(dirpath) unless File.exist?(dirpath)
         | 
| @@ -73,17 +166,32 @@ module CDNGet | |
| 73 166 | 
             
                    end
         | 
| 74 167 | 
             
                    puts() unless quiet
         | 
| 75 168 | 
             
                  end
         | 
| 169 | 
            +
                  http.close() if http
         | 
| 76 170 | 
             
                  nil
         | 
| 77 171 | 
             
                end
         | 
| 78 172 |  | 
| 79 173 | 
             
                protected
         | 
| 80 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 | 
            +
             | 
| 81 182 | 
             
                def fetch(url, library=nil)
         | 
| 82 183 | 
             
                  begin
         | 
| 83 | 
            -
                    html =  | 
| 184 | 
            +
                    html = http_get(url)
         | 
| 84 185 | 
             
                    return html
         | 
| 85 | 
            -
                  rescue OpenURI::HTTPError =>  | 
| 86 | 
            -
                     | 
| 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
         | 
| 87 195 | 
             
                  end
         | 
| 88 196 | 
             
                end
         | 
| 89 197 |  | 
| @@ -102,27 +210,21 @@ module CDNGet | |
| 102 210 | 
             
                  return value.to_s.reverse.scan(/..?.?/).collect {|s| s.reverse }.reverse.join(',')
         | 
| 103 211 | 
             
                end
         | 
| 104 212 |  | 
| 105 | 
            -
             | 
| 106 | 
            -
             | 
| 213 | 
            +
                def _debug_print(x)
         | 
| 214 | 
            +
                  if @debug_mode
         | 
| 215 | 
            +
                    $stderr.puts "\e[0;35m*** #{PP.pp(x,'')}\e[0m"
         | 
| 216 | 
            +
                  end
         | 
| 217 | 
            +
                end
         | 
| 107 218 |  | 
| 108 | 
            -
              class HttpError < StandardError
         | 
| 109 219 | 
             
              end
         | 
| 110 220 |  | 
| 111 221 |  | 
| 112 | 
            -
              class CDNJS < Base | 
| 222 | 
            +
              class CDNJS < Base
         | 
| 113 223 | 
             
                CODE = "cdnjs"
         | 
| 114 224 | 
             
                SITE_URL = 'https://cdnjs.com/'
         | 
| 115 225 |  | 
| 116 226 | 
             
                def fetch(url, library=nil)
         | 
| 117 | 
            -
                   | 
| 118 | 
            -
                    json_str = URI.open(url, 'rb') {|f| f.read() }
         | 
| 119 | 
            -
                  rescue OpenURI::HTTPError => exc
         | 
| 120 | 
            -
                    if exc.message == "404 Not Found"
         | 
| 121 | 
            -
                      json_str = "{}"
         | 
| 122 | 
            -
                    else
         | 
| 123 | 
            -
                      raise HttpError.new("GET #{url} : #{ex.message}")
         | 
| 124 | 
            -
                    end
         | 
| 125 | 
            -
                  end
         | 
| 227 | 
            +
                  json_str = super
         | 
| 126 228 | 
             
                  if json_str == "{}" && library
         | 
| 127 229 | 
             
                    if library.end_with?('js')
         | 
| 128 230 | 
             
                      maybe = library.end_with?('.js') \
         | 
| @@ -137,10 +239,10 @@ module CDNGet | |
| 137 239 | 
             
                end
         | 
| 138 240 | 
             
                protected :fetch
         | 
| 139 241 |  | 
| 140 | 
            -
                def list
         | 
| 141 | 
            -
                  libs = []
         | 
| 242 | 
            +
                def list()
         | 
| 142 243 | 
             
                  jstr = fetch("https://api.cdnjs.com/libraries?fields=name,description")
         | 
| 143 244 | 
             
                  jdata = JSON.parse(jstr)
         | 
| 245 | 
            +
                  _debug_print(jdata)
         | 
| 144 246 | 
             
                  libs = jdata['results'].collect {|d| {name: d['name'], desc: d['description']} }
         | 
| 145 247 | 
             
                  return libs.sort_by {|d| d[:name] }.uniq
         | 
| 146 248 | 
             
                end
         | 
| @@ -149,12 +251,16 @@ module CDNGet | |
| 149 251 | 
             
                  validate(library, nil)
         | 
| 150 252 | 
             
                  jstr = fetch("https://api.cdnjs.com/libraries/#{library}", library)
         | 
| 151 253 | 
             
                  jdata = JSON.parse(jstr)
         | 
| 254 | 
            +
                  _debug_print(jdata)
         | 
| 152 255 | 
             
                  versions = jdata['assets'].collect {|d| d['version'] }\
         | 
| 153 256 | 
             
                               .sort_by {|v| v.split(/[-.]/).map(&:to_i) }
         | 
| 154 257 | 
             
                  return {
         | 
| 155 258 | 
             
                    name: library,
         | 
| 156 259 | 
             
                    desc: jdata['description'],
         | 
| 157 260 | 
             
                    tags: (jdata['keywords'] || []).join(", "),
         | 
| 261 | 
            +
                    site: jdata['homepage'],
         | 
| 262 | 
            +
                    info: File.join(SITE_URL, "/libraries/#{library}"),
         | 
| 263 | 
            +
                    license: jdata['license'],
         | 
| 158 264 | 
             
                    versions: versions.reverse(),
         | 
| 159 265 | 
             
                  }
         | 
| 160 266 | 
             
                end
         | 
| @@ -163,17 +269,21 @@ module CDNGet | |
| 163 269 | 
             
                  validate(library, version)
         | 
| 164 270 | 
             
                  jstr = fetch("https://api.cdnjs.com/libraries/#{library}", library)
         | 
| 165 271 | 
             
                  jdata = JSON.parse(jstr)
         | 
| 272 | 
            +
                  _debug_print(jdata)
         | 
| 166 273 | 
             
                  d = jdata['assets'].find {|d| d['version'] == version }  or
         | 
| 167 274 | 
             
                    raise CommandError.new("#{library}/#{version}: Library or version not found.")
         | 
| 168 275 | 
             
                  baseurl = "https://cdnjs.cloudflare.com/ajax/libs/#{library}/#{version}/"
         | 
| 169 276 | 
             
                  return {
         | 
| 170 277 | 
             
                    name:     library,
         | 
| 278 | 
            +
                    version:  version,
         | 
| 171 279 | 
             
                    desc:     jdata['description'],
         | 
| 172 280 | 
             
                    tags:     (jdata['keywords'] || []).join(", "),
         | 
| 173 | 
            -
                     | 
| 281 | 
            +
                    site:     jdata['homepage'],
         | 
| 282 | 
            +
                    info:     File.join(SITE_URL, "/libraries/#{library}/#{version}"),
         | 
| 174 283 | 
             
                    urls:     d['files'].collect {|s| baseurl + s },
         | 
| 175 284 | 
             
                    files:    d['files'],
         | 
| 176 285 | 
             
                    baseurl:  baseurl,
         | 
| 286 | 
            +
                    license:  jdata['license'],
         | 
| 177 287 | 
             
                  }
         | 
| 178 288 | 
             
                end
         | 
| 179 289 |  | 
| @@ -183,59 +293,200 @@ module CDNGet | |
| 183 293 | 
             
              class JSDelivr < Base
         | 
| 184 294 | 
             
                CODE = "jsdelivr"
         | 
| 185 295 | 
             
                SITE_URL = "https://www.jsdelivr.com/"
         | 
| 186 | 
            -
                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 | 
            +
                }
         | 
| 187 302 |  | 
| 188 | 
            -
                def list
         | 
| 189 | 
            -
                   | 
| 190 | 
            -
             | 
| 191 | 
            -
             | 
| 192 | 
            -
             | 
| 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"]}
         | 
| 193 325 | 
             
                  }
         | 
| 194 326 | 
             
                end
         | 
| 195 327 |  | 
| 196 328 | 
             
                def find(library)
         | 
| 197 329 | 
             
                  validate(library, nil)
         | 
| 198 | 
            -
                   | 
| 199 | 
            -
                   | 
| 200 | 
            -
                   | 
| 201 | 
            -
                     | 
| 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
         | 
| 202 345 | 
             
                  return {
         | 
| 203 | 
            -
                    name: | 
| 204 | 
            -
                    desc: | 
| 205 | 
            -
                     | 
| 206 | 
            -
                    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'],
         | 
| 207 354 | 
             
                  }
         | 
| 208 355 | 
             
                end
         | 
| 209 356 |  | 
| 210 357 | 
             
                def get(library, version)
         | 
| 211 358 | 
             
                  validate(library, version)
         | 
| 212 | 
            -
                   | 
| 213 | 
            -
                   | 
| 214 | 
            -
             | 
| 215 | 
            -
                   | 
| 216 | 
            -
             | 
| 217 | 
            -
             | 
| 218 | 
            -
                   | 
| 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 | 
            +
                  #
         | 
| 219 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({
         | 
| 220 465 | 
             
                    name:     library,
         | 
| 221 466 | 
             
                    version:  version,
         | 
| 222 | 
            -
                     | 
| 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 },
         | 
| 223 470 | 
             
                    files:    files,
         | 
| 224 471 | 
             
                    baseurl:  baseurl,
         | 
| 225 | 
            -
             | 
| 472 | 
            +
                    default:  jdata["default"],
         | 
| 473 | 
            +
                    destdir:  "#{library}@#{version}",
         | 
| 474 | 
            +
                  })
         | 
| 475 | 
            +
                  return dict
         | 
| 226 476 | 
             
                end
         | 
| 227 477 |  | 
| 228 478 | 
             
              end
         | 
| 229 479 |  | 
| 230 480 |  | 
| 231 | 
            -
              class GoogleCDN < Base | 
| 481 | 
            +
              class GoogleCDN < Base
         | 
| 232 482 | 
             
                CODE = "google"
         | 
| 233 483 | 
             
                SITE_URL = 'https://developers.google.com/speed/libraries/'
         | 
| 234 484 |  | 
| 235 | 
            -
                def list
         | 
| 236 | 
            -
                  libs = []
         | 
| 485 | 
            +
                def list()
         | 
| 237 486 | 
             
                  html = fetch("https://developers.google.com/speed/libraries/")
         | 
| 487 | 
            +
                  _debug_print(html)
         | 
| 238 488 | 
             
                  rexp = %r`"https://ajax\.googleapis\.com/ajax/libs/([^/]+)/([^/]+)/([^"]+)"`
         | 
| 489 | 
            +
                  libs = []
         | 
| 239 490 | 
             
                  html.scan(rexp) do |lib, ver, file|
         | 
| 240 491 | 
             
                    libs << {name: lib, desc: "latest version: #{ver}" }
         | 
| 241 492 | 
             
                  end
         | 
| @@ -245,6 +496,7 @@ module CDNGet | |
| 245 496 | 
             
                def find(library)
         | 
| 246 497 | 
             
                  validate(library, nil)
         | 
| 247 498 | 
             
                  html = fetch("https://developers.google.com/speed/libraries/")
         | 
| 499 | 
            +
                  _debug_print(html)
         | 
| 248 500 | 
             
                  rexp = %r`"https://ajax\.googleapis\.com/ajax/libs/#{library}/`
         | 
| 249 501 | 
             
                  site_url = nil
         | 
| 250 502 | 
             
                  versions = []
         | 
| @@ -255,9 +507,7 @@ module CDNGet | |
| 255 507 | 
             
                      found = true
         | 
| 256 508 | 
             
                      if text =~ /<dt>.*?snippet:<\/dt>\s*<dd>(.*?)<\/dd>/m
         | 
| 257 509 | 
             
                        s = $1
         | 
| 258 | 
            -
                        s.scan(/\b(?:src|href)="([^"]*?)"/)  | 
| 259 | 
            -
                          urls << href
         | 
| 260 | 
            -
                        end
         | 
| 510 | 
            +
                        s.scan(/\b(?:src|href)="([^"]*?)"/) {|href,| urls << href }
         | 
| 261 511 | 
             
                      end
         | 
| 262 512 | 
             
                      if text =~ /<dt>site:<\/dt>\s*<dd>(.*?)<\/dd>/m
         | 
| 263 513 | 
             
                        s = $1
         | 
| @@ -278,6 +528,7 @@ module CDNGet | |
| 278 528 | 
             
                  return {
         | 
| 279 529 | 
             
                    name: library,
         | 
| 280 530 | 
             
                    site: site_url,
         | 
| 531 | 
            +
                    info: "#{SITE_URL}\##{library}",
         | 
| 281 532 | 
             
                    urls: urls,
         | 
| 282 533 | 
             
                    versions: versions,
         | 
| 283 534 | 
             
                  }
         | 
| @@ -298,6 +549,7 @@ module CDNGet | |
| 298 549 | 
             
                  return {
         | 
| 299 550 | 
             
                    name:    d[:name],
         | 
| 300 551 | 
             
                    site:    d[:site],
         | 
| 552 | 
            +
                    info:    "#{SITE_URL}\##{library}",
         | 
| 301 553 | 
             
                    urls:    urls,
         | 
| 302 554 | 
             
                    files:   files,
         | 
| 303 555 | 
             
                    baseurl: baseurl,
         | 
| @@ -330,24 +582,28 @@ module CDNGet | |
| 330 582 | 
             
                  @script = script || File.basename($0)
         | 
| 331 583 | 
             
                end
         | 
| 332 584 |  | 
| 333 | 
            -
                def help_message
         | 
| 585 | 
            +
                def help_message()
         | 
| 334 586 | 
             
                  script = @script
         | 
| 335 587 | 
             
                  return <<END
         | 
| 336 | 
            -
            #{script}  -- download files from public CDN
         | 
| 588 | 
            +
            #{script}  -- download files from public CDN (cdnjs/jsdelivr/unpkg/google)
         | 
| 337 589 |  | 
| 338 | 
            -
            Usage: #{script} [options] [CDN | 
| 590 | 
            +
            Usage: #{script} [<options>] [<CDN> [<library> [<version> [<directory>]]]]
         | 
| 339 591 |  | 
| 340 592 | 
             
            Options:
         | 
| 341 593 | 
             
              -h, --help        : help
         | 
| 342 594 | 
             
              -v, --version     : version
         | 
| 343 595 | 
             
              -q, --quiet       : minimal output
         | 
| 596 | 
            +
                  --debug       : (debug mode)
         | 
| 344 597 |  | 
| 345 598 | 
             
            Example:
         | 
| 346 | 
            -
              $ #{script}                                # list public CDN
         | 
| 599 | 
            +
              $ #{script}                                # list public CDN names
         | 
| 347 600 | 
             
              $ #{script} [-q] cdnjs                     # list libraries
         | 
| 601 | 
            +
              $ #{script} [-q] cdnjs 'jquery*'           # search libraries
         | 
| 348 602 | 
             
              $ #{script} [-q] cdnjs jquery              # list versions
         | 
| 349 | 
            -
              $ #{script} [-q] cdnjs jquery  | 
| 350 | 
            -
              $ #{script} [-q] cdnjs jquery  | 
| 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 | 
            +
             | 
| 351 607 | 
             
            END
         | 
| 352 608 | 
             
                end
         | 
| 353 609 |  | 
| @@ -362,10 +618,11 @@ END | |
| 362 618 | 
             
                end
         | 
| 363 619 |  | 
| 364 620 | 
             
                def run(*args)
         | 
| 365 | 
            -
                  cmdopts = parse_cmdopts(args, "hvq", ["help", "version", "quiet"])
         | 
| 621 | 
            +
                  cmdopts = parse_cmdopts(args, "hvq", ["help", "version", "quiet", "debug"])
         | 
| 366 622 | 
             
                  return help_message() if cmdopts['h'] || cmdopts['help']
         | 
| 367 623 | 
             
                  return RELEASE + "\n" if cmdopts['v'] || cmdopts['version']
         | 
| 368 624 | 
             
                  @quiet = cmdopts['quiet'] || cmdopts['q']
         | 
| 625 | 
            +
                  @debug_mode = cmdopts['debug']
         | 
| 369 626 | 
             
                  #
         | 
| 370 627 | 
             
                  validate(args[1], args[2])
         | 
| 371 628 | 
             
                  #
         | 
| @@ -377,11 +634,9 @@ END | |
| 377 634 | 
             
                    return do_list_libraries(cdn_code)
         | 
| 378 635 | 
             
                  when 2
         | 
| 379 636 | 
             
                    cdn_code, library = args
         | 
| 380 | 
            -
                     | 
| 381 | 
            -
             | 
| 382 | 
            -
             | 
| 383 | 
            -
                      return do_find_library(cdn_code, library)
         | 
| 384 | 
            -
                    end
         | 
| 637 | 
            +
                    return library.include?('*') \
         | 
| 638 | 
            +
                           ? do_search_libraries(cdn_code, library) \
         | 
| 639 | 
            +
                           : do_find_library(cdn_code, library)
         | 
| 385 640 | 
             
                  when 3
         | 
| 386 641 | 
             
                    cdn_code, library, version = args
         | 
| 387 642 | 
             
                    return do_get_library(cdn_code, library, version)
         | 
| @@ -435,35 +690,29 @@ END | |
| 435 690 | 
             
                def find_cdn(cdn_code)
         | 
| 436 691 | 
             
                  klass = CLASSES.find {|c| c::CODE == cdn_code }  or
         | 
| 437 692 | 
             
                    raise CommandError.new("#{cdn_code}: no such CDN.")
         | 
| 438 | 
            -
                  return klass.new
         | 
| 693 | 
            +
                  return klass.new(debug_mode: @debug_mode)
         | 
| 439 694 | 
             
                end
         | 
| 440 695 |  | 
| 441 696 | 
             
                def render_list(list)
         | 
| 442 | 
            -
                  if @quiet
         | 
| 443 | 
            -
             | 
| 444 | 
            -
                  else
         | 
| 445 | 
            -
                    return list.collect {|d| "%-20s  # %s\n" % [d[:name], d[:desc]] }.join()
         | 
| 446 | 
            -
                  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()
         | 
| 447 699 | 
             
                end
         | 
| 448 700 |  | 
| 449 | 
            -
                def do_list_cdns
         | 
| 450 | 
            -
                  if @quiet
         | 
| 451 | 
            -
             | 
| 452 | 
            -
                  else
         | 
| 453 | 
            -
                    return CLASSES.map {|c| "%-10s  # %s\n" % [c::CODE, c::SITE_URL] }.join()
         | 
| 454 | 
            -
                  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()
         | 
| 455 704 | 
             
                end
         | 
| 456 705 |  | 
| 457 706 | 
             
                def do_list_libraries(cdn_code)
         | 
| 458 707 | 
             
                  cdn = find_cdn(cdn_code)
         | 
| 459 | 
            -
                   | 
| 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)
         | 
| 460 711 | 
             
                end
         | 
| 461 712 |  | 
| 462 713 | 
             
                def do_search_libraries(cdn_code, pattern)
         | 
| 463 714 | 
             
                  cdn = find_cdn(cdn_code)
         | 
| 464 | 
            -
                   | 
| 465 | 
            -
                  rexp = Regexp.compile("\\A#{rexp_str}\\z", Regexp::IGNORECASE)
         | 
| 466 | 
            -
                  return render_list(cdn.list.select {|a| a[:name] =~ rexp })
         | 
| 715 | 
            +
                  return render_list(cdn.search(pattern))
         | 
| 467 716 | 
             
                end
         | 
| 468 717 |  | 
| 469 718 | 
             
                def do_find_library(cdn_code, library)
         | 
| @@ -475,10 +724,12 @@ END | |
| 475 724 | 
             
                      s << "#{ver}\n"
         | 
| 476 725 | 
             
                    end if d[:versions]
         | 
| 477 726 | 
             
                  else
         | 
| 478 | 
            -
                    s << "name: | 
| 479 | 
            -
                    s << "desc: | 
| 480 | 
            -
                    s << "tags: | 
| 481 | 
            -
                    s << "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]
         | 
| 482 733 | 
             
                    s << "snippet: |\n" << d[:snippet].gsub(/^/, '    ') if d[:snippet]
         | 
| 483 734 | 
             
                    s << "versions:\n"
         | 
| 484 735 | 
             
                    d[:versions].each do |ver|
         | 
| @@ -490,6 +741,7 @@ END | |
| 490 741 |  | 
| 491 742 | 
             
                def do_get_library(cdn_code, library, version)
         | 
| 492 743 | 
             
                  cdn = find_cdn(cdn_code)
         | 
| 744 | 
            +
                  version = _latest_version(cdn, library) if version == 'latest'
         | 
| 493 745 | 
             
                  d = cdn.get(library, version)
         | 
| 494 746 | 
             
                  s = ""
         | 
| 495 747 | 
             
                  if @quiet
         | 
| @@ -499,9 +751,13 @@ END | |
| 499 751 | 
             
                  else
         | 
| 500 752 | 
             
                    s << "name:     #{d[:name]}\n"
         | 
| 501 753 | 
             
                    s << "version:  #{d[:version]}\n"
         | 
| 502 | 
            -
                    s << "desc:     #{d[:desc]}\n" | 
| 503 | 
            -
                    s << "tags:     #{d[:tags]}\n" | 
| 504 | 
            -
                    s << "site:     #{d[:site]}\n" | 
| 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]
         | 
| 505 761 | 
             
                    s << "snippet: |\n" << d[:snippet].gsub(/^/, '    ') if d[:snippet]
         | 
| 506 762 | 
             
                    s << "urls:\n"  if d[:urls]
         | 
| 507 763 | 
             
                    d[:urls].each do |url|
         | 
| @@ -513,10 +769,18 @@ END | |
| 513 769 |  | 
| 514 770 | 
             
                def do_download_library(cdn_code, library, version, basedir)
         | 
| 515 771 | 
             
                  cdn = find_cdn(cdn_code)
         | 
| 772 | 
            +
                  version = _latest_version(cdn, library) if version == 'latest'
         | 
| 516 773 | 
             
                  cdn.download(library, version, basedir, quiet: @quiet)
         | 
| 517 774 | 
             
                  return nil
         | 
| 518 775 | 
             
                end
         | 
| 519 776 |  | 
| 777 | 
            +
                private
         | 
| 778 | 
            +
             | 
| 779 | 
            +
                def _latest_version(cdn, library)
         | 
| 780 | 
            +
                  d = cdn.find(library)
         | 
| 781 | 
            +
                  return d[:versions].first
         | 
| 782 | 
            +
                end
         | 
| 783 | 
            +
             | 
| 520 784 | 
             
              end
         | 
| 521 785 |  | 
| 522 786 |  |