docs.thor 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. class DocsCLI < Thor
  2. include Thor::Actions
  3. def self.to_s
  4. 'Docs'
  5. end
  6. def initialize(*args)
  7. require 'docs'
  8. trap('INT') { puts; exit! } # hide backtrace on ^C
  9. super
  10. end
  11. desc 'list', 'List available documentations'
  12. def list
  13. max_length = 0
  14. Docs.all.
  15. map { |doc| [doc.to_s.demodulize.underscore, doc] }.
  16. to_h.
  17. each { |name, doc| max_length = name.length if name.length > max_length }.
  18. each { |name, doc| puts "#{name.rjust max_length + 1}: #{doc.versions.map { |v| v.release || '-' }.join(', ')}" }
  19. end
  20. desc 'page <doc> [path] [--version] [--verbose] [--debug]', 'Generate a page (no indexing)'
  21. option :version, type: :string
  22. option :verbose, type: :boolean
  23. option :debug, type: :boolean
  24. def page(name, path = '')
  25. unless path.empty? || path.start_with?('/')
  26. return puts 'ERROR: [path] must be an absolute path.'
  27. end
  28. Docs.install_report :store if options[:verbose]
  29. if options[:debug]
  30. GC.disable
  31. Docs.install_report :filter, :request
  32. end
  33. if Docs.generate_page(name, options[:version], path)
  34. puts 'Done'
  35. else
  36. puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}"
  37. end
  38. rescue Docs::DocNotFound => error
  39. handle_doc_not_found_error(error)
  40. end
  41. desc 'generate <doc> [--version] [--verbose] [--debug] [--force] [--package]', 'Generate a documentation'
  42. option :version, type: :string
  43. option :verbose, type: :boolean
  44. option :debug, type: :boolean
  45. option :force, type: :boolean
  46. option :package, type: :boolean
  47. def generate(name)
  48. Docs.install_report :store if options[:verbose]
  49. Docs.install_report :scraper if options[:debug]
  50. Docs.install_report :progress_bar, :doc if $stdout.tty?
  51. unless options[:force]
  52. puts <<-TEXT.strip_heredoc
  53. Note: this command will scrape the documentation from the source.
  54. Some scrapers require a local setup. Others will send thousands of
  55. HTTP requests, potentially slowing down the source site.
  56. Please don't use it unless you are modifying the code.
  57. To download the latest tested version of a documentation, use:
  58. thor docs:download #{name}\n
  59. TEXT
  60. return unless yes? 'Proceed? (y/n)'
  61. end
  62. if Docs.generate(name, options[:version])
  63. generate_manifest
  64. if options[:package]
  65. require 'unix_utils'
  66. package_doc(Docs.find(name, options[:version]))
  67. end
  68. puts 'Done'
  69. else
  70. puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}"
  71. end
  72. rescue Docs::DocNotFound => error
  73. handle_doc_not_found_error(error)
  74. end
  75. desc 'manifest', 'Create the manifest'
  76. def manifest
  77. generate_manifest
  78. puts 'Done'
  79. end
  80. desc 'download (<doc> <doc@version>... | --all)', 'Download documentations'
  81. option :all, type: :boolean
  82. def download(*names)
  83. require 'unix_utils'
  84. docs = options[:all] ? Docs.all : find_docs(names)
  85. assert_docs(docs)
  86. download_docs(docs)
  87. generate_manifest
  88. puts 'Done'
  89. rescue Docs::DocNotFound => error
  90. handle_doc_not_found_error(error)
  91. end
  92. desc 'package <doc> <doc@version>...', 'Package documentations'
  93. def package(*names)
  94. require 'unix_utils'
  95. docs = find_docs(names)
  96. assert_docs(docs)
  97. docs.each(&method(:package_doc))
  98. puts 'Done'
  99. rescue Docs::DocNotFound => error
  100. handle_doc_not_found_error(error)
  101. end
  102. desc 'clean', 'Delete documentation packages'
  103. def clean
  104. File.delete(*Dir[File.join Docs.store_path, '*.tar.gz'])
  105. puts 'Done'
  106. end
  107. private
  108. def find_docs(names)
  109. names.map do |name|
  110. name, version = name.split('@')
  111. Docs.find(name, version)
  112. end
  113. end
  114. def assert_docs(docs)
  115. if docs.empty?
  116. puts 'ERROR: called with no arguments.'
  117. puts 'Run "thor docs:list" for usage patterns.'
  118. exit
  119. end
  120. end
  121. def handle_doc_not_found_error(error)
  122. puts %(ERROR: #{error}.)
  123. puts 'Run "thor docs:list" to see the list of docs and versions.'
  124. end
  125. def download_docs(docs)
  126. # Don't allow downloaded files to be created as StringIO
  127. require 'open-uri'
  128. OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax')
  129. OpenURI::Buffer.const_set 'StringMax', 0
  130. require 'thread'
  131. length = docs.length
  132. i = 0
  133. (1..4).map do
  134. Thread.new do
  135. while doc = docs.shift
  136. status = begin
  137. download_doc(doc)
  138. 'OK'
  139. rescue => e
  140. "FAILED (#{e.class}: #{e.message})"
  141. end
  142. puts "(#{i += 1}/#{length}) #{doc.name} #{status}"
  143. end
  144. end
  145. end.map(&:join)
  146. end
  147. def download_doc(doc)
  148. target = File.join(Docs.store_path, "#{doc.path}.tar.gz")
  149. open "http://dl.devdocs.io/#{doc.path}.tar.gz" do |file|
  150. FileUtils.mkpath(Docs.store_path)
  151. FileUtils.mv(file, target)
  152. unpackage_doc(doc)
  153. end
  154. end
  155. def unpackage_doc(doc)
  156. path = File.join(Docs.store_path, doc.path)
  157. FileUtils.mkpath(path)
  158. tar = UnixUtils.gunzip("#{path}.tar.gz")
  159. dir = UnixUtils.untar(tar)
  160. FileUtils.rm_rf(path)
  161. FileUtils.mv(dir, path)
  162. FileUtils.rm(tar)
  163. FileUtils.rm("#{path}.tar.gz")
  164. end
  165. def package_doc(doc)
  166. path = File.join Docs.store_path, doc.path
  167. if File.exist?(path)
  168. tar = UnixUtils.tar(path)
  169. gzip = UnixUtils.gzip(tar)
  170. FileUtils.mv(gzip, "#{path}.tar.gz")
  171. FileUtils.rm(tar)
  172. else
  173. puts %(ERROR: can't find "#{doc.name}" documentation files.)
  174. end
  175. end
  176. def generate_manifest
  177. Docs.generate_manifest
  178. end
  179. end