docs.thor 7.1 KB

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