docs.thor 8.3 KB

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