docs.thor 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  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>... | --all)', 'Package documentations'
  93. option :all, type: :boolean
  94. def package(*names)
  95. require 'unix_utils'
  96. docs = options[:all] ? Docs.all : find_docs(names)
  97. assert_docs(docs)
  98. docs.each(&method(:package_doc))
  99. puts 'Done'
  100. rescue Docs::DocNotFound => error
  101. handle_doc_not_found_error(error)
  102. end
  103. desc 'clean', 'Delete documentation packages'
  104. def clean
  105. File.delete(*Dir[File.join Docs.store_path, '*.tar.gz'])
  106. puts 'Done'
  107. end
  108. private
  109. def find_docs(names)
  110. names.map do |name|
  111. name, version = name.split('@')
  112. Docs.find(name, version)
  113. end
  114. end
  115. def assert_docs(docs)
  116. if docs.empty?
  117. puts 'ERROR: called with no arguments.'
  118. puts 'Run "thor docs:list" for usage patterns.'
  119. exit
  120. end
  121. end
  122. def handle_doc_not_found_error(error)
  123. puts %(ERROR: #{error}.)
  124. puts 'Run "thor docs:list" to see the list of docs and versions.'
  125. end
  126. def download_docs(docs)
  127. # Don't allow downloaded files to be created as StringIO
  128. require 'open-uri'
  129. OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax')
  130. OpenURI::Buffer.const_set 'StringMax', 0
  131. require 'thread'
  132. length = docs.length
  133. i = 0
  134. (1..4).map do
  135. Thread.new do
  136. while doc = docs.shift
  137. status = begin
  138. download_doc(doc)
  139. 'OK'
  140. rescue => e
  141. "FAILED (#{e.class}: #{e.message})"
  142. end
  143. puts "(#{i += 1}/#{length}) #{doc.name} #{status}"
  144. end
  145. end
  146. end.map(&:join)
  147. end
  148. def download_doc(doc)
  149. target = File.join(Docs.store_path, "#{doc.path}.tar.gz")
  150. open "http://dl.devdocs.io/#{doc.path}.tar.gz" do |file|
  151. FileUtils.mkpath(Docs.store_path)
  152. FileUtils.mv(file, target)
  153. unpackage_doc(doc)
  154. end
  155. end
  156. def unpackage_doc(doc)
  157. path = File.join(Docs.store_path, doc.path)
  158. FileUtils.mkpath(path)
  159. tar = UnixUtils.gunzip("#{path}.tar.gz")
  160. dir = UnixUtils.untar(tar)
  161. FileUtils.rm_rf(path)
  162. FileUtils.mv(dir, path)
  163. FileUtils.rm(tar)
  164. FileUtils.rm("#{path}.tar.gz")
  165. end
  166. def package_doc(doc)
  167. path = File.join Docs.store_path, doc.path
  168. if File.exist?(path)
  169. tar = UnixUtils.tar(path)
  170. gzip = UnixUtils.gzip(tar)
  171. FileUtils.mv(gzip, "#{path}.tar.gz")
  172. FileUtils.rm(tar)
  173. else
  174. puts %(ERROR: can't find "#{doc.name}" documentation files.)
  175. end
  176. end
  177. def generate_manifest
  178. Docs.generate_manifest
  179. end
  180. end