scint 0.1.0
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/FEATURES.md +13 -0
- data/README.md +216 -0
- data/bin/bundler-vs-scint +233 -0
- data/bin/scint +35 -0
- data/bin/scint-io-summary +46 -0
- data/bin/scint-syscall-trace +41 -0
- data/lib/bundler/setup.rb +5 -0
- data/lib/bundler.rb +168 -0
- data/lib/scint/cache/layout.rb +131 -0
- data/lib/scint/cache/metadata_store.rb +75 -0
- data/lib/scint/cache/prewarm.rb +192 -0
- data/lib/scint/cli/add.rb +85 -0
- data/lib/scint/cli/cache.rb +316 -0
- data/lib/scint/cli/exec.rb +150 -0
- data/lib/scint/cli/install.rb +1047 -0
- data/lib/scint/cli/remove.rb +60 -0
- data/lib/scint/cli.rb +77 -0
- data/lib/scint/commands/exec.rb +17 -0
- data/lib/scint/commands/install.rb +17 -0
- data/lib/scint/credentials.rb +153 -0
- data/lib/scint/debug/io_trace.rb +218 -0
- data/lib/scint/debug/sampler.rb +138 -0
- data/lib/scint/downloader/fetcher.rb +113 -0
- data/lib/scint/downloader/pool.rb +112 -0
- data/lib/scint/errors.rb +63 -0
- data/lib/scint/fs.rb +119 -0
- data/lib/scint/gem/extractor.rb +86 -0
- data/lib/scint/gem/package.rb +62 -0
- data/lib/scint/gemfile/dependency.rb +30 -0
- data/lib/scint/gemfile/editor.rb +93 -0
- data/lib/scint/gemfile/parser.rb +275 -0
- data/lib/scint/index/cache.rb +166 -0
- data/lib/scint/index/client.rb +301 -0
- data/lib/scint/index/parser.rb +142 -0
- data/lib/scint/installer/extension_builder.rb +264 -0
- data/lib/scint/installer/linker.rb +226 -0
- data/lib/scint/installer/planner.rb +140 -0
- data/lib/scint/installer/preparer.rb +207 -0
- data/lib/scint/lockfile/parser.rb +251 -0
- data/lib/scint/lockfile/writer.rb +178 -0
- data/lib/scint/platform.rb +71 -0
- data/lib/scint/progress.rb +579 -0
- data/lib/scint/resolver/provider.rb +230 -0
- data/lib/scint/resolver/resolver.rb +249 -0
- data/lib/scint/runtime/exec.rb +141 -0
- data/lib/scint/runtime/setup.rb +45 -0
- data/lib/scint/scheduler.rb +392 -0
- data/lib/scint/source/base.rb +46 -0
- data/lib/scint/source/git.rb +92 -0
- data/lib/scint/source/path.rb +70 -0
- data/lib/scint/source/rubygems.rb +79 -0
- data/lib/scint/vendor/pub_grub/assignment.rb +20 -0
- data/lib/scint/vendor/pub_grub/basic_package_source.rb +169 -0
- data/lib/scint/vendor/pub_grub/failure_writer.rb +182 -0
- data/lib/scint/vendor/pub_grub/incompatibility.rb +150 -0
- data/lib/scint/vendor/pub_grub/package.rb +43 -0
- data/lib/scint/vendor/pub_grub/partial_solution.rb +121 -0
- data/lib/scint/vendor/pub_grub/rubygems.rb +45 -0
- data/lib/scint/vendor/pub_grub/solve_failure.rb +19 -0
- data/lib/scint/vendor/pub_grub/static_package_source.rb +61 -0
- data/lib/scint/vendor/pub_grub/strategy.rb +42 -0
- data/lib/scint/vendor/pub_grub/term.rb +105 -0
- data/lib/scint/vendor/pub_grub/version.rb +3 -0
- data/lib/scint/vendor/pub_grub/version_constraint.rb +129 -0
- data/lib/scint/vendor/pub_grub/version_range.rb +423 -0
- data/lib/scint/vendor/pub_grub/version_solver.rb +236 -0
- data/lib/scint/vendor/pub_grub/version_union.rb +178 -0
- data/lib/scint/vendor/pub_grub.rb +32 -0
- data/lib/scint/worker_pool.rb +114 -0
- data/lib/scint.rb +87 -0
- metadata +116 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dependency"
|
|
4
|
+
|
|
5
|
+
module Scint
|
|
6
|
+
module Gemfile
|
|
7
|
+
# Result of parsing a Gemfile.
|
|
8
|
+
ParseResult = Struct.new(:dependencies, :sources, :ruby_version, :platforms, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
# Evaluates a Gemfile using instance_eval, just like stock bundler.
|
|
11
|
+
# Supports the full Gemfile DSL: source, gem, group, platforms, git_source,
|
|
12
|
+
# git, path, eval_gemfile, ruby, gemspec, and user-defined methods.
|
|
13
|
+
class Parser
|
|
14
|
+
def self.parse(gemfile_path)
|
|
15
|
+
parser = new(gemfile_path)
|
|
16
|
+
parser.evaluate
|
|
17
|
+
ParseResult.new(
|
|
18
|
+
dependencies: parser.parsed_dependencies,
|
|
19
|
+
sources: parser.parsed_sources.uniq,
|
|
20
|
+
ruby_version: parser.parsed_ruby_version,
|
|
21
|
+
platforms: parser.parsed_platforms,
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Accessors that don't collide with DSL method names
|
|
26
|
+
def parsed_dependencies; @dependencies; end
|
|
27
|
+
def parsed_sources; @sources; end
|
|
28
|
+
def parsed_ruby_version; @ruby_version; end
|
|
29
|
+
def parsed_platforms; @declared_platforms; end
|
|
30
|
+
|
|
31
|
+
def initialize(gemfile_path)
|
|
32
|
+
@gemfile_path = File.expand_path(gemfile_path)
|
|
33
|
+
@dependencies = []
|
|
34
|
+
@sources = []
|
|
35
|
+
@git_sources = {}
|
|
36
|
+
@current_groups = []
|
|
37
|
+
@current_platforms = []
|
|
38
|
+
@current_source_options = {}
|
|
39
|
+
@ruby_version = nil
|
|
40
|
+
@declared_platforms = []
|
|
41
|
+
|
|
42
|
+
add_default_git_sources
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def evaluate
|
|
46
|
+
contents = File.read(@gemfile_path)
|
|
47
|
+
instance_eval(contents, @gemfile_path, 1)
|
|
48
|
+
rescue SyntaxError => e
|
|
49
|
+
raise GemfileError, "Syntax error in #{File.basename(@gemfile_path)}: #{e.message}"
|
|
50
|
+
rescue ScriptError, StandardError => e
|
|
51
|
+
raise GemfileError, "Error evaluating #{File.basename(@gemfile_path)}: #{e.message}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# --- Gemfile DSL methods ---
|
|
55
|
+
|
|
56
|
+
def source(url, &blk)
|
|
57
|
+
url = url.to_s
|
|
58
|
+
if block_given?
|
|
59
|
+
old_source = @current_source_options.dup
|
|
60
|
+
@current_source_options = { source: url }
|
|
61
|
+
@sources << { type: :rubygems, uri: url }
|
|
62
|
+
yield
|
|
63
|
+
@current_source_options = old_source
|
|
64
|
+
else
|
|
65
|
+
@sources << { type: :rubygems, uri: url }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def gem(name, *args)
|
|
70
|
+
options = args.last.is_a?(Hash) ? args.pop.dup : {}
|
|
71
|
+
version_reqs = args.flatten
|
|
72
|
+
|
|
73
|
+
# Collect groups
|
|
74
|
+
groups = @current_groups.dup
|
|
75
|
+
if options[:group] || options[:groups]
|
|
76
|
+
extra = Array(options.delete(:group)) + Array(options.delete(:groups))
|
|
77
|
+
groups.concat(extra.map(&:to_sym))
|
|
78
|
+
end
|
|
79
|
+
groups = [:default] if groups.empty?
|
|
80
|
+
|
|
81
|
+
# Collect platforms
|
|
82
|
+
plats = @current_platforms.dup
|
|
83
|
+
if options[:platform] || options[:platforms]
|
|
84
|
+
extra = Array(options.delete(:platform)) + Array(options.delete(:platforms))
|
|
85
|
+
plats.concat(extra.map(&:to_sym))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Handle require paths
|
|
89
|
+
require_paths = nil
|
|
90
|
+
if options.key?(:require)
|
|
91
|
+
req = options.delete(:require)
|
|
92
|
+
require_paths = req == false ? [] : Array(req)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Build source options from git_source helpers and explicit options
|
|
96
|
+
source_opts = @current_source_options.dup
|
|
97
|
+
|
|
98
|
+
# Handle custom git sources (e.g. shopify: "repo-name")
|
|
99
|
+
@git_sources.each do |src_name, block|
|
|
100
|
+
if options.key?(src_name)
|
|
101
|
+
repo = options.delete(src_name)
|
|
102
|
+
result = block.call(repo)
|
|
103
|
+
if result.is_a?(Hash)
|
|
104
|
+
source_opts.merge!(result.transform_keys(&:to_sym))
|
|
105
|
+
else
|
|
106
|
+
source_opts[:git] = result.to_s
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Handle explicit git/github/path/source options
|
|
112
|
+
if options[:github]
|
|
113
|
+
repo = options.delete(:github)
|
|
114
|
+
# Handle pull request URLs
|
|
115
|
+
if repo =~ %r{\Ahttps://github\.com/([^/]+/[^/]+)/pull/(\d+)\z}
|
|
116
|
+
source_opts[:git] = "https://github.com/#{$1}.git"
|
|
117
|
+
source_opts[:ref] = "refs/pull/#{$2}/head"
|
|
118
|
+
else
|
|
119
|
+
repo = "#{repo}/#{repo}" unless repo.include?("/")
|
|
120
|
+
source_opts[:git] = "https://github.com/#{repo}.git"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if options[:git]
|
|
125
|
+
source_opts[:git] = options.delete(:git)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if options[:path]
|
|
129
|
+
path_val = options.delete(:path)
|
|
130
|
+
# Resolve relative paths against the Gemfile's directory
|
|
131
|
+
unless path_val.start_with?("/")
|
|
132
|
+
path_val = File.expand_path(path_val, File.dirname(@gemfile_path))
|
|
133
|
+
end
|
|
134
|
+
source_opts[:path] = path_val
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if options[:source]
|
|
138
|
+
source_opts[:source] = options.delete(:source)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Copy over git-related options
|
|
142
|
+
[:branch, :ref, :tag, :submodules].each do |key|
|
|
143
|
+
source_opts[key] = options.delete(key) if options.key?(key)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Ignore options we don't use but shouldn't error on
|
|
147
|
+
options.delete(:force_ruby_platform)
|
|
148
|
+
options.delete(:install_if)
|
|
149
|
+
|
|
150
|
+
dep = Dependency.new(
|
|
151
|
+
name,
|
|
152
|
+
version_reqs: version_reqs,
|
|
153
|
+
groups: groups,
|
|
154
|
+
platforms: plats,
|
|
155
|
+
require_paths: require_paths,
|
|
156
|
+
source_options: source_opts,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@dependencies << dep
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def group(*names, **opts, &blk)
|
|
163
|
+
old_groups = @current_groups.dup
|
|
164
|
+
@current_groups.concat(names.map(&:to_sym))
|
|
165
|
+
yield
|
|
166
|
+
ensure
|
|
167
|
+
@current_groups = old_groups
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def platforms(*names, &blk)
|
|
171
|
+
old_platforms = @current_platforms.dup
|
|
172
|
+
@current_platforms.concat(names.map(&:to_sym))
|
|
173
|
+
yield
|
|
174
|
+
ensure
|
|
175
|
+
@current_platforms = old_platforms
|
|
176
|
+
end
|
|
177
|
+
alias_method :platform, :platforms
|
|
178
|
+
|
|
179
|
+
def git_source(name, &block)
|
|
180
|
+
raise GemfileError, "git_source requires a block" unless block_given?
|
|
181
|
+
@git_sources[name.to_sym] = block
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def git(url, **opts, &blk)
|
|
185
|
+
raise GemfileError, "git requires a block" unless block_given?
|
|
186
|
+
old_source = @current_source_options.dup
|
|
187
|
+
@current_source_options = { git: url }.merge(opts)
|
|
188
|
+
yield
|
|
189
|
+
ensure
|
|
190
|
+
@current_source_options = old_source
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def path(path_str, **opts, &blk)
|
|
194
|
+
old_source = @current_source_options.dup
|
|
195
|
+
resolved = if path_str.start_with?("/")
|
|
196
|
+
path_str
|
|
197
|
+
else
|
|
198
|
+
File.expand_path(path_str, File.dirname(@gemfile_path))
|
|
199
|
+
end
|
|
200
|
+
@current_source_options = { path: resolved }.merge(opts)
|
|
201
|
+
yield if block_given?
|
|
202
|
+
ensure
|
|
203
|
+
@current_source_options = old_source
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def eval_gemfile(path)
|
|
207
|
+
expanded = if path.start_with?("/")
|
|
208
|
+
path
|
|
209
|
+
else
|
|
210
|
+
File.expand_path(path, File.dirname(@gemfile_path))
|
|
211
|
+
end
|
|
212
|
+
contents = File.read(expanded)
|
|
213
|
+
instance_eval(contents, expanded, 1)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def ruby(version, **opts)
|
|
217
|
+
@ruby_version = version.to_s
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def gemspec(opts = {})
|
|
221
|
+
path = opts[:path] || "."
|
|
222
|
+
name = opts[:name]
|
|
223
|
+
dir = File.expand_path(path, File.dirname(@gemfile_path))
|
|
224
|
+
gemspecs = Dir.glob(File.join(dir, "{,*}.gemspec"))
|
|
225
|
+
# Just record we have a gemspec source -- full spec loading is
|
|
226
|
+
# deferred to the resolver/installer.
|
|
227
|
+
gemspecs.each do |gs|
|
|
228
|
+
spec_name = File.basename(gs, ".gemspec")
|
|
229
|
+
next if name && spec_name != name
|
|
230
|
+
gem(spec_name, path: dir)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Silently ignore plugin declarations
|
|
235
|
+
def plugin(*args); end
|
|
236
|
+
|
|
237
|
+
# Allow user-defined methods (like `in_repo_gem`) and unknown DSL
|
|
238
|
+
# methods to raise a clear error.
|
|
239
|
+
def method_missing(name, *args, &block)
|
|
240
|
+
raise GemfileError, "Undefined local variable or method `#{name}' for Gemfile\n" \
|
|
241
|
+
" in #{@gemfile_path}"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def respond_to_missing?(name, include_private = false)
|
|
245
|
+
false
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
private
|
|
249
|
+
|
|
250
|
+
def add_default_git_sources
|
|
251
|
+
git_source(:github) do |repo_name|
|
|
252
|
+
if repo_name =~ %r{\Ahttps://github\.com/([^/]+/[^/]+)/pull/(\d+)\z}
|
|
253
|
+
{
|
|
254
|
+
git: "https://github.com/#{$1}.git",
|
|
255
|
+
ref: "refs/pull/#{$2}/head",
|
|
256
|
+
}
|
|
257
|
+
else
|
|
258
|
+
repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
|
|
259
|
+
"https://github.com/#{repo_name}.git"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
git_source(:gist) do |repo_name|
|
|
264
|
+
"https://gist.github.com/#{repo_name}.git"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
git_source(:bitbucket) do |repo_name|
|
|
268
|
+
user, repo = repo_name.split("/")
|
|
269
|
+
repo ||= user
|
|
270
|
+
"https://#{user}@bitbucket.org/#{user}/#{repo}.git"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "digest"
|
|
5
|
+
|
|
6
|
+
module Scint
|
|
7
|
+
module Index
|
|
8
|
+
class Cache
|
|
9
|
+
attr_reader :directory
|
|
10
|
+
|
|
11
|
+
def initialize(cache_dir)
|
|
12
|
+
@directory = File.expand_path(cache_dir)
|
|
13
|
+
@info_dir = File.join(@directory, "info")
|
|
14
|
+
@info_etag_dir = File.join(@directory, "info-etags")
|
|
15
|
+
@info_binary_dir = File.join(@directory, "info-binary")
|
|
16
|
+
@mutex = Thread::Mutex.new
|
|
17
|
+
|
|
18
|
+
ensure_dirs
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Source slug for cache directory naming.
|
|
22
|
+
# e.g. "rubygems.org" or "gems.example.com-private"
|
|
23
|
+
def self.slug_for(uri)
|
|
24
|
+
uri = URI.parse(uri.to_s) unless uri.is_a?(URI)
|
|
25
|
+
path = uri.path.to_s.gsub("/", "-").sub(/^-/, "")
|
|
26
|
+
slug = uri.host.to_s
|
|
27
|
+
slug += path unless path.empty? || path == "-"
|
|
28
|
+
slug
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Read cached names file.
|
|
32
|
+
def names
|
|
33
|
+
read_file(names_path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Write names file.
|
|
37
|
+
def write_names(data, etag: nil)
|
|
38
|
+
write_file(names_path, data)
|
|
39
|
+
write_file(names_etag_path, etag) if etag
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Read the ETag for names.
|
|
43
|
+
def names_etag
|
|
44
|
+
read_file(names_etag_path)&.chomp
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Read cached versions file.
|
|
48
|
+
def versions
|
|
49
|
+
read_file(versions_path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Write versions file. Supports appending for range requests.
|
|
53
|
+
def write_versions(data, etag: nil, append: false)
|
|
54
|
+
if append && File.exist?(versions_path)
|
|
55
|
+
File.open(versions_path, "ab") { |f| f.write(data) }
|
|
56
|
+
else
|
|
57
|
+
write_file(versions_path, data)
|
|
58
|
+
end
|
|
59
|
+
write_file(versions_etag_path, etag) if etag
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Read the ETag for versions.
|
|
63
|
+
def versions_etag
|
|
64
|
+
read_file(versions_etag_path)&.chomp
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Size of the versions file (for Range requests).
|
|
68
|
+
def versions_size
|
|
69
|
+
File.exist?(versions_path) ? File.size(versions_path) : 0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Read cached info for a gem.
|
|
73
|
+
def info(name)
|
|
74
|
+
read_file(info_path(name))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Write info for a gem.
|
|
78
|
+
def write_info(name, data, etag: nil)
|
|
79
|
+
write_file(info_path(name), data)
|
|
80
|
+
write_file(info_etag_path(name), etag) if etag
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Read the ETag for a gem's info.
|
|
84
|
+
def info_etag(name)
|
|
85
|
+
read_file(info_etag_path(name))&.chomp
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check local checksum of info file against remote.
|
|
89
|
+
def info_fresh?(name, remote_checksum)
|
|
90
|
+
return false unless remote_checksum && !remote_checksum.empty?
|
|
91
|
+
data = info(name)
|
|
92
|
+
return false unless data
|
|
93
|
+
local_checksum = Digest::MD5.hexdigest(data)
|
|
94
|
+
local_checksum == remote_checksum
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Read binary (Marshal) cached parsed info. Returns nil if missing/stale.
|
|
98
|
+
def read_binary_info(name, expected_checksum)
|
|
99
|
+
path = info_binary_path(name)
|
|
100
|
+
return nil unless File.exist?(path)
|
|
101
|
+
|
|
102
|
+
cached = Marshal.load(File.binread(path)) # rubocop:disable Security/MarshalLoad
|
|
103
|
+
if cached.is_a?(Array) && cached.length == 2 && cached[0] == expected_checksum
|
|
104
|
+
cached[1]
|
|
105
|
+
end
|
|
106
|
+
rescue StandardError
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Write binary (Marshal) cached parsed info.
|
|
111
|
+
def write_binary_info(name, checksum, parsed_data)
|
|
112
|
+
path = info_binary_path(name)
|
|
113
|
+
FS.mkdir_p(File.dirname(path))
|
|
114
|
+
FS.atomic_write(path, Marshal.dump([checksum, parsed_data]))
|
|
115
|
+
rescue StandardError
|
|
116
|
+
# Non-fatal
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def names_path = File.join(@directory, "names")
|
|
122
|
+
def names_etag_path = File.join(@directory, "names.etag")
|
|
123
|
+
def versions_path = File.join(@directory, "versions")
|
|
124
|
+
def versions_etag_path = File.join(@directory, "versions.etag")
|
|
125
|
+
|
|
126
|
+
def info_path(name)
|
|
127
|
+
name = name.to_s
|
|
128
|
+
if /[^a-z0-9_-]/.match?(name)
|
|
129
|
+
File.join(@info_dir, "#{name}-#{hex(name)}")
|
|
130
|
+
else
|
|
131
|
+
File.join(@info_dir, name)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def info_etag_path(name)
|
|
136
|
+
name = name.to_s
|
|
137
|
+
File.join(@info_etag_dir, "#{name}-#{hex(name)}")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def info_binary_path(name)
|
|
141
|
+
File.join(@info_binary_dir, "#{name}.bin")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def hex(str)
|
|
145
|
+
Digest::MD5.hexdigest(str)[0, 12]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def ensure_dirs
|
|
149
|
+
[@directory, @info_dir, @info_etag_dir, @info_binary_dir].each do |dir|
|
|
150
|
+
FS.mkdir_p(dir)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def read_file(path)
|
|
155
|
+
return nil unless File.exist?(path)
|
|
156
|
+
File.read(path)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def write_file(path, data)
|
|
160
|
+
return if data.nil?
|
|
161
|
+
FS.mkdir_p(File.dirname(path))
|
|
162
|
+
FS.atomic_write(path, data)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|