docs.thor 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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. output = Docs.all.flat_map do |doc|
  14. name = doc.to_s.demodulize.underscore
  15. if doc.versioned?
  16. doc.versions.map { |_doc| "#{name}@#{_doc.version}" }
  17. else
  18. name
  19. end
  20. end.join("\n")
  21. require 'tty-pager'
  22. TTY::Pager.new.page(output)
  23. end
  24. desc 'page <doc> [path] [--version] [--verbose] [--debug]', 'Generate a page (no indexing)'
  25. option :version, type: :string
  26. option :verbose, type: :boolean
  27. option :debug, type: :boolean
  28. def page(name, path = '')
  29. unless path.empty? || path.start_with?('/')
  30. return puts 'ERROR: [path] must be an absolute path.'
  31. end
  32. Docs.install_report :image
  33. Docs.install_report :store if options[:verbose]
  34. if options[:debug]
  35. GC.disable
  36. Docs.install_report :filter, :request
  37. end
  38. if Docs.generate_page(name, options[:version], path)
  39. puts 'Done'
  40. else
  41. puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}"
  42. end
  43. rescue Docs::DocNotFound => error
  44. handle_doc_not_found_error(error)
  45. end
  46. desc 'generate <doc> [--version] [--verbose] [--debug] [--force] [--package]', 'Generate a documentation'
  47. option :version, type: :string
  48. option :all, type: :boolean
  49. option :verbose, type: :boolean
  50. option :debug, type: :boolean
  51. option :force, type: :boolean
  52. option :package, type: :boolean
  53. def generate(name)
  54. Docs.rescue_errors = true
  55. Docs.install_report :store if options[:verbose]
  56. Docs.install_report :scraper if options[:debug]
  57. Docs.install_report :progress_bar, :doc, :image if $stdout.tty?
  58. require 'unix_utils' if options[:package]
  59. doc = Docs.find(name, options[:version])
  60. if doc < Docs::UrlScraper && !options[:force]
  61. puts <<-TEXT.strip_heredoc
  62. /!\\ WARNING /!\\
  63. Some scrapers send thousands of HTTP requests in a short period of time,
  64. which can slow down the source site and trouble its maintainers.
  65. Please scrape responsibly. Don't do it unless you're modifying the code.
  66. To download the latest tested version of this documentation, run:
  67. thor docs:download #{name}\n
  68. TEXT
  69. return unless yes? 'Proceed? (y/n)'
  70. end
  71. result = if doc.version && options[:all]
  72. doc.superclass.versions.all? do |_doc|
  73. puts "==> #{_doc.version}"
  74. generate_doc(_doc, package: options[:package]).tap { puts "\n" }
  75. end
  76. else
  77. generate_doc(doc, package: options[:package])
  78. end
  79. generate_manifest if result
  80. rescue Docs::DocNotFound => error
  81. handle_doc_not_found_error(error)
  82. ensure
  83. Docs.rescue_errors = false
  84. end
  85. desc 'manifest', 'Create the manifest'
  86. def manifest
  87. generate_manifest
  88. puts 'Done'
  89. end
  90. desc 'download (<doc> <doc@version>... | --default | --installed)', 'Download documentations'
  91. option :default, type: :boolean
  92. option :installed, type: :boolean
  93. option :all, type: :boolean
  94. def download(*names)
  95. require 'unix_utils'
  96. docs = if options[:default]
  97. Docs.defaults
  98. elsif options[:installed]
  99. Docs.installed
  100. elsif options[:all]
  101. Docs.all_versions
  102. else
  103. find_docs(names)
  104. end
  105. assert_docs(docs)
  106. download_docs(docs)
  107. generate_manifest
  108. puts 'Done'
  109. rescue Docs::DocNotFound => error
  110. handle_doc_not_found_error(error)
  111. end
  112. desc 'package <doc> <doc@version>...', 'Package documentations'
  113. def package(*names)
  114. require 'unix_utils'
  115. docs = find_docs(names)
  116. assert_docs(docs)
  117. docs.each(&method(:package_doc))
  118. puts 'Done'
  119. rescue Docs::DocNotFound => error
  120. handle_doc_not_found_error(error)
  121. end
  122. desc 'clean', 'Delete documentation packages'
  123. def clean
  124. File.delete(*Dir[File.join Docs.store_path, '*.tar.gz'])
  125. puts 'Done'
  126. end
  127. desc 'upload', '[private]'
  128. option :dryrun, type: :boolean
  129. option :packaged, type: :boolean
  130. def upload(*names)
  131. names = Dir[File.join(Docs.store_path, '*.tar.gz')].map { |f| File.basename(f, '.tar.gz') } if options[:packaged]
  132. docs = find_docs(names)
  133. assert_docs(docs)
  134. docs.each do |doc|
  135. puts "Syncing #{doc.path}..."
  136. cmd = "aws s3 sync #{File.join(Docs.store_path, doc.path)} s3://docs.devdocs.io/#{doc.path} --delete"
  137. cmd << ' --dryrun' if options[:dryrun]
  138. system(cmd)
  139. end
  140. end
  141. desc 'commit', '[private]'
  142. option :message, type: :string
  143. option :amend, type: :boolean
  144. def commit(name)
  145. doc = Docs.find(name, false)
  146. message = options[:message] || "Update #{doc.name} documentation (#{doc.versions.first.release})"
  147. amend = " --amend" if options[:amend]
  148. system("git add assets/ *#{name}*") && system("git commit -m '#{message}'#{amend}")
  149. rescue Docs::DocNotFound => error
  150. handle_doc_not_found_error(error)
  151. end
  152. private
  153. def find_docs(names)
  154. names.map do |name|
  155. name, version = name.split(/@|~/)
  156. Docs.find(name, version)
  157. end
  158. end
  159. def assert_docs(docs)
  160. if docs.empty?
  161. puts 'ERROR: called with no arguments.'
  162. puts 'Run "thor list" for usage patterns.'
  163. exit
  164. end
  165. end
  166. def handle_doc_not_found_error(error)
  167. puts %(ERROR: #{error}.)
  168. puts 'Run "thor docs:list" to see the list of docs and versions.'
  169. end
  170. def generate_doc(doc, package: nil)
  171. if Docs.generate(doc)
  172. package_doc(doc) if package
  173. puts 'Done'
  174. true
  175. else
  176. puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}"
  177. false
  178. end
  179. end
  180. def download_docs(docs)
  181. # Don't allow downloaded files to be created as StringIO
  182. require 'open-uri'
  183. OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax')
  184. OpenURI::Buffer.const_set 'StringMax', 0
  185. require 'thread'
  186. length = docs.length
  187. mutex = Mutex.new
  188. i = 0
  189. (1..4).map do
  190. Thread.new do
  191. while doc = docs.shift
  192. status = begin
  193. download_doc(doc)
  194. 'OK'
  195. rescue => e
  196. "FAILED (#{e.class}: #{e.message})"
  197. end
  198. mutex.synchronize { puts "(#{i += 1}/#{length}) #{doc.name}#{ " #{doc.version}" if doc.version} #{status}" }
  199. end
  200. end
  201. end.map(&:join)
  202. end
  203. def download_doc(doc)
  204. target_path = File.join(Docs.store_path, doc.path)
  205. open "http://dl.devdocs.io/#{doc.path}.tar.gz" do |file|
  206. FileUtils.mkpath(target_path)
  207. file.close
  208. tar = UnixUtils.gunzip(file.path)
  209. dir = UnixUtils.untar(tar)
  210. FileUtils.rm_rf(target_path)
  211. FileUtils.mv(dir, target_path)
  212. FileUtils.rm(file.path)
  213. end
  214. end
  215. def package_doc(doc)
  216. path = File.join Docs.store_path, doc.path
  217. if File.exist?(path)
  218. tar = UnixUtils.tar(path)
  219. gzip = UnixUtils.gzip(tar)
  220. FileUtils.mv(gzip, "#{path}.tar.gz")
  221. FileUtils.rm(tar)
  222. else
  223. puts %(ERROR: can't find "#{doc.name}" documentation files.)
  224. end
  225. end
  226. def generate_manifest
  227. Docs.generate_manifest
  228. end
  229. end