kmc 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +22 -0
- data/README.md +141 -0
- data/Rakefile +2 -0
- data/bin/kmc +20 -0
- data/kmc.gemspec +26 -0
- data/lib/kmc.rb +27 -0
- data/lib/kmc/configuration.rb +67 -0
- data/lib/kmc/download_url.rb +80 -0
- data/lib/kmc/git_adapter.rb +71 -0
- data/lib/kmc/package.rb +142 -0
- data/lib/kmc/package_attrs.rb +69 -0
- data/lib/kmc/package_downloads.rb +122 -0
- data/lib/kmc/package_dsl.rb +13 -0
- data/lib/kmc/package_utils.rb +29 -0
- data/lib/kmc/packages/lack_luster_labs.rb +22 -0
- data/lib/kmc/page_fetcher.js +47 -0
- data/lib/kmc/post_processors/module_manager_resolver.rb +32 -0
- data/lib/kmc/refresher.rb +36 -0
- data/lib/kmc/user_interface.rb +195 -0
- data/lib/kmc/util.rb +11 -0
- data/lib/kmc/version.rb +3 -0
- data/lib/kmc/versioner.rb +70 -0
- data/lib/kmc/web/app.rb +42 -0
- data/lib/kmc/web/public/css/main.css +69 -0
- data/lib/kmc/web/public/css/normalize.css +527 -0
- data/lib/kmc/web/public/index.html +107 -0
- data/lib/kmc/web/public/js/main.js +125 -0
- data/lib/kmc/web/public/js/plugins.js +24 -0
- data/lib/kmc/web/public/js/vendor/jquery-1.10.2.min.js +6 -0
- data/lib/kmc/web/public/js/vendor/modernizr-2.6.2.min.js +4 -0
- data/lib/kmc/web/public/js/vendor/underscore.js +5 -0
- data/package_generator.rb +197 -0
- data/spec/download_url_spec.rb +69 -0
- data/spec/fixtures/example_box.html +165 -0
- data/spec/fixtures/example_dropbox.com.html +114 -0
- data/spec/fixtures/example_mediafire.html +101 -0
- data/spec/package_spec.rb +113 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/versioner_spec.rb +69 -0
- metadata +177 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
module Kmc
|
2
|
+
module GitAdapter
|
3
|
+
class << self
|
4
|
+
def init_repo(path)
|
5
|
+
Dir.chdir(path) do
|
6
|
+
`git init`
|
7
|
+
`git config user.name KMC`
|
8
|
+
`git config user.email kmc@kmc.kmc`
|
9
|
+
`git config core.autocrlf false`
|
10
|
+
|
11
|
+
File.open('.gitignore', 'w') do |file|
|
12
|
+
file.write "!*\n"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def commit_everything(repo_path, commit_message)
|
18
|
+
Dir.chdir(repo_path) do
|
19
|
+
`git add -A -f`
|
20
|
+
`git commit --allow-empty -m "#{commit_message}"`
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def revert_commit(repo_path, commit)
|
25
|
+
Dir.chdir(repo_path) do
|
26
|
+
# Favor "ours" (which is always HEAD for our purposes) when git-revert
|
27
|
+
# can handle that on its own.
|
28
|
+
`git revert --no-commit --strategy=merge --strategy-option=ours #{commit.sha}`
|
29
|
+
|
30
|
+
# When files are being created or deleted, git will not do anything.
|
31
|
+
# In this case, keep all files alive; better to accidentally pollute
|
32
|
+
# than accidentally delete something important.
|
33
|
+
`git add *`
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def list_commits(repo_path)
|
38
|
+
Dir.chdir(repo_path) do
|
39
|
+
`git log --oneline`.lines.map do |line|
|
40
|
+
sha, message = line.split(' ', 2)
|
41
|
+
Commit.new(message, sha)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Commit < Struct.new(:message, :sha)
|
48
|
+
def pre?
|
49
|
+
type == :pre
|
50
|
+
end
|
51
|
+
|
52
|
+
def post?
|
53
|
+
type == :post
|
54
|
+
end
|
55
|
+
|
56
|
+
def uninstall?
|
57
|
+
type == :uninstall
|
58
|
+
end
|
59
|
+
|
60
|
+
def type
|
61
|
+
# "POST: Example" --> :post
|
62
|
+
message.split(':').first.downcase.to_sym
|
63
|
+
end
|
64
|
+
|
65
|
+
def subject
|
66
|
+
# "POST: Example\n" --> "Example"
|
67
|
+
message.split(' ', 2).last.strip
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/kmc/package.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'httparty'
|
3
|
+
require 'zip'
|
4
|
+
require 'tmpdir'
|
5
|
+
require 'damerau-levenshtein'
|
6
|
+
|
7
|
+
require_relative 'package_utils'
|
8
|
+
|
9
|
+
module Kmc
|
10
|
+
class Package
|
11
|
+
include PackageDsl
|
12
|
+
|
13
|
+
class << self
|
14
|
+
include PackageAttrs, PackageUtils
|
15
|
+
|
16
|
+
attr_reader :ksp_path, :download_dir, :do_not_unzip
|
17
|
+
|
18
|
+
# Installs a list of packages, and outputs caveats when it's done.
|
19
|
+
def install_packages!(ksp_path, packages)
|
20
|
+
caveats = {}
|
21
|
+
packages.each { |package| package.install!(ksp_path, caveats) }
|
22
|
+
log_caveats(caveats)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Internal version of the `install` method. Handles:
|
26
|
+
# * Pre- and post-requisites
|
27
|
+
# * Version control
|
28
|
+
# * Post-processors
|
29
|
+
# * Calling a package's #install method
|
30
|
+
# * Building up caveats
|
31
|
+
#
|
32
|
+
# The caveats argument is expected to be a hash going from Packages to
|
33
|
+
# caveat messages and will be modified in-place.
|
34
|
+
def install!(ksp_path, caveats)
|
35
|
+
return if Versioner.already_installed?(ksp_path, self)
|
36
|
+
|
37
|
+
Util.log "Installing package #{self.title}"
|
38
|
+
prepare_for_install(ksp_path, caveats)
|
39
|
+
|
40
|
+
Util.log "Saving your work before installing ..."
|
41
|
+
Versioner.mark_preinstall(ksp_path, self)
|
42
|
+
|
43
|
+
Util.log "Installing #{title} ..."
|
44
|
+
self.new.install
|
45
|
+
|
46
|
+
Util.log "Cleaning up ..."
|
47
|
+
Util.run_post_processors!(ksp_path)
|
48
|
+
|
49
|
+
Versioner.mark_postinstall(ksp_path, self)
|
50
|
+
|
51
|
+
Util.log "Done!"
|
52
|
+
|
53
|
+
install_postrequisites!(caveats)
|
54
|
+
end
|
55
|
+
|
56
|
+
def download_and_unzip!
|
57
|
+
PackageDownloads.download_and_unzip_package(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def download!
|
61
|
+
PackageDownloads.download_package(self)
|
62
|
+
end
|
63
|
+
|
64
|
+
def do_not_unzip!
|
65
|
+
@do_not_unzip = true
|
66
|
+
end
|
67
|
+
|
68
|
+
# a callback for when a subclass of this class is created
|
69
|
+
def inherited(package)
|
70
|
+
(@@packages ||= []) << package
|
71
|
+
end
|
72
|
+
|
73
|
+
def packages
|
74
|
+
@@packages
|
75
|
+
end
|
76
|
+
|
77
|
+
def load_packages!
|
78
|
+
Dir[File.join(Configuration.packages_path, '*.rb')].each do |file|
|
79
|
+
require file
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# Run steps that take place before installation.
|
87
|
+
def prepare_for_install(ksp_path, caveats)
|
88
|
+
add_caveat_message!(caveats)
|
89
|
+
@ksp_path = ksp_path
|
90
|
+
install_prerequisites!(caveats)
|
91
|
+
@download_dir = download_and_unzip!
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_caveat_message!(caveats)
|
95
|
+
if method_defined?(:caveats)
|
96
|
+
caveats.merge!(self => self.new.caveats)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def install_prerequisites!(caveats)
|
101
|
+
resolve_prerequisites.each do |package|
|
102
|
+
unless Versioner.already_installed?(ksp_path, package)
|
103
|
+
Util.log "#{title} has prerequisite #{package.title}."
|
104
|
+
package.install!(ksp_path, caveats)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def install_postrequisites!(caveats)
|
110
|
+
resolve_postrequisites.each do |package|
|
111
|
+
unless Versioner.already_installed?(ksp_path, package)
|
112
|
+
Util.log "#{title} has postrequisite #{package.title}."
|
113
|
+
package.install!(ksp_path, caveats)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def log_caveats(caveats)
|
119
|
+
if caveats.any?
|
120
|
+
Util.log "===> Caveats"
|
121
|
+
|
122
|
+
caveats.each do |package, message|
|
123
|
+
Util.log <<-EOS.undent
|
124
|
+
Caveat from #{package.title}:
|
125
|
+
#{message}
|
126
|
+
EOS
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def load_post_processors!
|
132
|
+
processors_path = File.join(File.dirname(__FILE__), 'post_processors')
|
133
|
+
Dir["#{processors_path}/*.rb"].each do |file|
|
134
|
+
require file
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
load_packages!
|
140
|
+
load_post_processors!
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Kmc
|
2
|
+
module PackageAttrs
|
3
|
+
def title(title = nil)
|
4
|
+
if title
|
5
|
+
@title = title
|
6
|
+
else
|
7
|
+
@title
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def url(url = nil)
|
12
|
+
if url
|
13
|
+
@url = url
|
14
|
+
else
|
15
|
+
@url
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def aliases(*aliases)
|
20
|
+
@aliases ||= []
|
21
|
+
|
22
|
+
if aliases.any?
|
23
|
+
@aliases = aliases
|
24
|
+
else
|
25
|
+
@aliases
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def names
|
30
|
+
[title, aliases].flatten
|
31
|
+
end
|
32
|
+
|
33
|
+
def prerequisites(*prerequisites)
|
34
|
+
@prerequisites ||= []
|
35
|
+
|
36
|
+
if prerequisites.any?
|
37
|
+
@prerequisites = prerequisites
|
38
|
+
else
|
39
|
+
@prerequisites
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
alias_method :prerequisite, :prerequisites
|
44
|
+
alias_method :pre_requisite, :prerequisites
|
45
|
+
alias_method :pre_requisites, :prerequisites
|
46
|
+
|
47
|
+
def postrequisites(*postrequisites)
|
48
|
+
@postrequisites ||= []
|
49
|
+
|
50
|
+
if postrequisites.any?
|
51
|
+
@postrequisites = postrequisites
|
52
|
+
else
|
53
|
+
@postrequisites
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
alias_method :postrequisite, :postrequisites
|
58
|
+
alias_method :post_requisite, :postrequisites
|
59
|
+
alias_method :post_requisites, :postrequisites
|
60
|
+
|
61
|
+
def resolve_prerequisites
|
62
|
+
prerequisites.map { |package_name| find(package_name) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def resolve_postrequisites
|
66
|
+
postrequisites.map { |package_name| find(package_name) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Kmc
|
2
|
+
module PackageDownloads
|
3
|
+
class << self
|
4
|
+
# Downloads and unzips a package. This will call #download! on its own, and
|
5
|
+
# will return the location where the package was downloaded to as a
|
6
|
+
# Pathname.
|
7
|
+
def download_and_unzip_package(package, opts = {})
|
8
|
+
download_file = download_package(package, opts)
|
9
|
+
output_path = Pathname.new(download_file.path).parent.to_s
|
10
|
+
|
11
|
+
return output_path if package.do_not_unzip
|
12
|
+
|
13
|
+
Util.log "Unzipping ..."
|
14
|
+
unzip_file(download_file.path, output_path)
|
15
|
+
|
16
|
+
File.delete(File.absolute_path(download_file))
|
17
|
+
|
18
|
+
output_path
|
19
|
+
end
|
20
|
+
|
21
|
+
# Downloads the zipfile for a package using its URL, unless a cached version
|
22
|
+
# is found first. Uses DownloadUrl to intelligently resolve download URLs.
|
23
|
+
#
|
24
|
+
# Returns the file downloaded, which is created in a temp directory.
|
25
|
+
def download_package(package, opts = {})
|
26
|
+
download_uri, downloaded_file = fetch_package_file(package)
|
27
|
+
|
28
|
+
save_to_cache(package, downloaded_file) if opts[:cache_after_download]
|
29
|
+
|
30
|
+
download_to_tempdir(download_file_name(download_uri), downloaded_file)
|
31
|
+
end
|
32
|
+
|
33
|
+
def unzip_file(zipfile_path, output_path)
|
34
|
+
Zip::File.open(zipfile_path) do |zip_file|
|
35
|
+
zip_file.each do |entry|
|
36
|
+
destination = File.join(output_path, entry.name)
|
37
|
+
parent_dir = File.expand_path('..', destination)
|
38
|
+
|
39
|
+
FileUtils.mkdir_p(parent_dir) unless File.exists?(parent_dir)
|
40
|
+
|
41
|
+
entry.extract(destination)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Places `file_contents` into a file called `file_name` in a temporary
|
47
|
+
# directory.
|
48
|
+
def download_to_tempdir(file_name, file_contents)
|
49
|
+
tmpdir = Dir.mktmpdir
|
50
|
+
|
51
|
+
file = File.new(File.join(tmpdir, file_name), 'wb+')
|
52
|
+
file.write(file_contents)
|
53
|
+
file.close
|
54
|
+
|
55
|
+
file
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Given a package, returns a two-entry array.
|
61
|
+
#
|
62
|
+
# The first entry is a String uri pointing to where the file was fetched
|
63
|
+
# from (either the filesystem or a website), and the second entry is the
|
64
|
+
# contents of the download.
|
65
|
+
def fetch_package_file(package)
|
66
|
+
cache = cache_file(package)
|
67
|
+
if cache
|
68
|
+
Util.log "Using a cached version of #{package.title} ..."
|
69
|
+
|
70
|
+
[cache, File.read(cache)]
|
71
|
+
else
|
72
|
+
Util.log "The package is found at #{package.url}."
|
73
|
+
Util.log "Finding the download URL ..."
|
74
|
+
|
75
|
+
download_uri = resolve_download_url(package)
|
76
|
+
|
77
|
+
Util.log "Found it. Downloading from #{download_uri} ..."
|
78
|
+
[download_uri, HTTParty.get(download_uri)]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the location of the cached version of a package, or some falsy
|
83
|
+
# value if no such file exists.
|
84
|
+
def cache_file(package)
|
85
|
+
if Kmc::Configuration.cache_dir
|
86
|
+
cache_file = File.join(Kmc::Configuration.cache_dir,
|
87
|
+
"#{package.title}.zip")
|
88
|
+
|
89
|
+
File.file?(cache_file) && cache_file
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def resolve_download_url(package)
|
94
|
+
DownloadUrl.new(package.url).resolve_download_url
|
95
|
+
end
|
96
|
+
|
97
|
+
# Writes a downloaded package to the cache.
|
98
|
+
#
|
99
|
+
# This method assumes `Kmc.cache_dir` already exists.
|
100
|
+
def save_to_cache(package, downloaded_file)
|
101
|
+
Util.log "Saving #{package.title} to cache ..."
|
102
|
+
|
103
|
+
cache_location = File.join(Kmc.cache_dir, "#{package.title}.zip")
|
104
|
+
File.open(cache_location, 'wb+') do |file|
|
105
|
+
file.write(downloaded_file)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
VALID_CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
|
110
|
+
"0123456789-._~:/?#[]@!$&'()*+,;=").chars
|
111
|
+
|
112
|
+
# Converts a string URI into the file name a downloaded file should be
|
113
|
+
# placed into.
|
114
|
+
def download_file_name(uri)
|
115
|
+
# Remove invalid characters to ensure a valid URI.
|
116
|
+
clean_uri = uri.chars.select { |char| VALID_CHARS.include?(char) }.join
|
117
|
+
|
118
|
+
File.basename(URI(clean_uri).path)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Kmc
|
2
|
+
module PackageDsl
|
3
|
+
def merge_directory(from, opts = {})
|
4
|
+
destination = opts[:into] || '.'
|
5
|
+
|
6
|
+
FileUtils.cp_r(File.join(self.class.download_dir, from),
|
7
|
+
File.join(self.class.ksp_path, destination))
|
8
|
+
end
|
9
|
+
def remove_filepath(filepath)
|
10
|
+
FileUtils.rm_r(File.join(self.class.ksp_path, filepath))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Kmc
|
2
|
+
module PackageUtils
|
3
|
+
# Lowercases and hyphenates a package name; this is the format packages
|
4
|
+
# are expected to be supplied as when passed from the user.
|
5
|
+
def normalize_for_find(name)
|
6
|
+
name.downcase.gsub(/[ \-]+/, "-")
|
7
|
+
end
|
8
|
+
|
9
|
+
def normalized_title
|
10
|
+
normalize_for_find(title)
|
11
|
+
end
|
12
|
+
|
13
|
+
def find(name)
|
14
|
+
packages.find do |package|
|
15
|
+
package.names.any? do |candidate_name|
|
16
|
+
normalize_for_find(candidate_name) == normalize_for_find(name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def search(name)
|
22
|
+
packages.min_by do |package|
|
23
|
+
package.names.map do |candidate_name|
|
24
|
+
DamerauLevenshtein.distance(name, candidate_name)
|
25
|
+
end.min
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|