docs.thor 6.2 KB

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