anyway_config 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,6 +8,7 @@ module Anyway # :nodoc:
8
8
  using Anyway::Ext::DeepDup
9
9
  using Anyway::Ext::DeepFreeze
10
10
  using Anyway::Ext::Hash
11
+ using Anyway::Ext::FlattenNames
11
12
 
12
13
  using(Module.new do
13
14
  refine Object do
@@ -26,6 +27,7 @@ module Anyway # :nodoc:
26
27
  RESERVED_NAMES = %i[
27
28
  config_name
28
29
  env_prefix
30
+ as_env
29
31
  values
30
32
  class
31
33
  clear
@@ -47,8 +49,6 @@ module Anyway # :nodoc:
47
49
  __type_caster__
48
50
  ].freeze
49
51
 
50
- ENV_OPTION_EXCLUDE_KEY = :except
51
-
52
52
  class Error < StandardError; end
53
53
 
54
54
  class ValidationError < Error; end
@@ -128,34 +128,14 @@ module Anyway # :nodoc:
128
128
  end
129
129
  end
130
130
 
131
- def required(*names, env: nil)
132
- unknown_names = names - config_attributes
131
+ def required(*names, env: nil, **nested)
132
+ unknown_names = names + nested.keys - config_attributes
133
133
  raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
134
134
 
135
- names = filter_by_env(names, env)
136
- required_attributes.push(*names)
137
- end
135
+ return unless Settings.matching_env?(env)
138
136
 
139
- def filter_by_env(names, env)
140
- return names if env.nil? || env.to_s == current_env
141
-
142
- filtered_names = if env.is_a?(Hash)
143
- names_with_exclude_env_option(names, env)
144
- elsif env.is_a?(Array)
145
- names if env.flat_map(&:to_s).include?(current_env)
146
- end
147
-
148
- filtered_names || []
149
- end
150
-
151
- def current_env
152
- Settings.current_environment.to_s
153
- end
154
-
155
- def names_with_exclude_env_option(names, env)
156
- envs = env[ENV_OPTION_EXCLUDE_KEY]
157
- excluded_envs = [envs].flat_map(&:to_s)
158
- names if excluded_envs.none?(current_env)
137
+ required_attributes.push(*names)
138
+ required_attributes.push(*nested.flatten_names)
159
139
  end
160
140
 
161
141
  def required_attributes
@@ -223,6 +203,12 @@ module Anyway # :nodoc:
223
203
 
224
204
  def coerce_types(mapping)
225
205
  Utils.deep_merge!(coercion_mapping, mapping)
206
+
207
+ mapping.each do |key, val|
208
+ next unless val == :boolean || (val.is_a?(::Hash) && val[:type] == :boolean)
209
+
210
+ alias_method :"#{key}?", :"#{key}"
211
+ end
226
212
  end
227
213
 
228
214
  def coercion_mapping
@@ -268,7 +254,7 @@ module Anyway # :nodoc:
268
254
  names.each do |name|
269
255
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
270
256
  def #{name}=(val)
271
- __trace__&.record_value(val, \"#{name}\", **Tracing.current_trace_source)
257
+ __trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
272
258
  values[:#{name}] = val
273
259
  end
274
260
 
@@ -297,7 +283,7 @@ module Anyway # :nodoc:
297
283
  # - SomeModule::Config => "some_module"
298
284
  # - SomeConfig => "some"
299
285
  unless name =~ /^(\w+)(::)?Config$/
300
- raise "Couldn't infer config name, please, specify it explicitly" \
286
+ raise "Couldn't infer config name, please, specify it explicitly " \
301
287
  "via `config_name :my_config`"
302
288
  end
303
289
 
@@ -430,13 +416,18 @@ module Anyway # :nodoc:
430
416
  end
431
417
  end
432
418
 
419
+ def as_env
420
+ Env.from_hash(to_h, prefix: env_prefix)
421
+ end
422
+
433
423
  private
434
424
 
435
425
  attr_reader :values, :__trace__
436
426
 
437
427
  def validate_required_attributes!
438
428
  self.class.required_attributes.select do |name|
439
- values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
429
+ val = values.dig(*name.to_s.split(".").map(&:to_sym))
430
+ val.nil? || (val.is_a?(String) && val.empty?)
440
431
  end.then do |missing|
441
432
  next if missing.empty?
442
433
  raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
@@ -8,14 +8,31 @@ module Anyway
8
8
  using Anyway::Ext::DeepDup
9
9
  using Anyway::Ext::Hash
10
10
 
11
+ class << self
12
+ def from_hash(hash, prefix: nil, memo: {})
13
+ hash.each do |key, value|
14
+ prefix_with_key = (prefix && !prefix.empty?) ? "#{prefix}_#{key.to_s.upcase}" : key.to_s.upcase
15
+
16
+ if value.is_a?(Hash)
17
+ from_hash(value, prefix: "#{prefix_with_key}_", memo: memo)
18
+ else
19
+ memo[prefix_with_key] = value.to_s
20
+ end
21
+ end
22
+
23
+ memo
24
+ end
25
+ end
26
+
11
27
  include Tracing
12
28
 
13
- attr_reader :data, :traces, :type_cast
29
+ attr_reader :data, :traces, :type_cast, :env_container
14
30
 
15
- def initialize(type_cast: AutoCast)
31
+ def initialize(type_cast: AutoCast, env_container: ENV)
16
32
  @type_cast = type_cast
17
33
  @data = {}
18
34
  @traces = {}
35
+ @env_container = env_container
19
36
  end
20
37
 
21
38
  def clear
@@ -42,11 +59,11 @@ module Anyway
42
59
  private
43
60
 
44
61
  def parse_env(prefix)
45
- match_prefix = "#{prefix}_"
46
- ENV.each_pair.with_object({}) do |(key, val), data|
62
+ match_prefix = prefix.empty? ? prefix : "#{prefix}_"
63
+ env_container.each_pair.with_object({}) do |(key, val), data|
47
64
  next unless key.start_with?(match_prefix)
48
65
 
49
- path = key.sub(/^#{prefix}_/, "").downcase
66
+ path = key.sub(/^#{match_prefix}/, "").downcase
50
67
 
51
68
  paths = path.split("__")
52
69
  trace!(:env, *paths, key: key) { data.bury(type_cast.call(val), *paths) }
@@ -19,7 +19,7 @@ module Anyway
19
19
  def initialize(type = :trace, value = UNDEF, **source)
20
20
  @type = type
21
21
  @source = source
22
- @value = value == UNDEF ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
22
+ @value = (value == UNDEF) ? Hash.new { |h, k| h[k] = Trace.new(:trace) } : value
23
23
  end
24
24
 
25
25
  def dig(...)
@@ -35,7 +35,7 @@ module Anyway
35
35
  end
36
36
 
37
37
  target_trace = path.empty? ? self : value.dig(*path)
38
- target_trace.value[key.to_s] = trace
38
+ target_trace.record_key(key.to_s, trace)
39
39
 
40
40
  val
41
41
  end
@@ -54,6 +54,12 @@ module Anyway
54
54
  hash
55
55
  end
56
56
 
57
+ def record_key(key, key_trace)
58
+ @value = Hash.new { |h, k| h[k] = Trace.new(:trace) } unless value.is_a?(::Hash)
59
+
60
+ value[key] = key_trace
61
+ end
62
+
57
63
  def merge!(another_trace)
58
64
  raise ArgumentError, "You can only merge into a :trace type, and this is :#{type}" unless trace?
59
65
  raise ArgumentError, "You can only merge a :trace type, but trying :#{type}" unless another_trace.trace?
data/lib/anyway/config.rb CHANGED
@@ -8,6 +8,7 @@ module Anyway # :nodoc:
8
8
  using Anyway::Ext::DeepDup
9
9
  using Anyway::Ext::DeepFreeze
10
10
  using Anyway::Ext::Hash
11
+ using Anyway::Ext::FlattenNames
11
12
 
12
13
  using(Module.new do
13
14
  refine Object do
@@ -26,6 +27,7 @@ module Anyway # :nodoc:
26
27
  RESERVED_NAMES = %i[
27
28
  config_name
28
29
  env_prefix
30
+ as_env
29
31
  values
30
32
  class
31
33
  clear
@@ -47,8 +49,6 @@ module Anyway # :nodoc:
47
49
  __type_caster__
48
50
  ].freeze
49
51
 
50
- ENV_OPTION_EXCLUDE_KEY = :except
51
-
52
52
  class Error < StandardError; end
53
53
 
54
54
  class ValidationError < Error; end
@@ -128,34 +128,14 @@ module Anyway # :nodoc:
128
128
  end
129
129
  end
130
130
 
131
- def required(*names, env: nil)
132
- unknown_names = names - config_attributes
131
+ def required(*names, env: nil, **nested)
132
+ unknown_names = names + nested.keys - config_attributes
133
133
  raise ArgumentError, "Unknown config param: #{unknown_names.join(",")}" if unknown_names.any?
134
134
 
135
- names = filter_by_env(names, env)
136
- required_attributes.push(*names)
137
- end
135
+ return unless Settings.matching_env?(env)
138
136
 
139
- def filter_by_env(names, env)
140
- return names if env.nil? || env.to_s == current_env
141
-
142
- filtered_names = if env.is_a?(Hash)
143
- names_with_exclude_env_option(names, env)
144
- elsif env.is_a?(Array)
145
- names if env.flat_map(&:to_s).include?(current_env)
146
- end
147
-
148
- filtered_names || []
149
- end
150
-
151
- def current_env
152
- Settings.current_environment.to_s
153
- end
154
-
155
- def names_with_exclude_env_option(names, env)
156
- envs = env[ENV_OPTION_EXCLUDE_KEY]
157
- excluded_envs = [envs].flat_map(&:to_s)
158
- names if excluded_envs.none?(current_env)
137
+ required_attributes.push(*names)
138
+ required_attributes.push(*nested.flatten_names)
159
139
  end
160
140
 
161
141
  def required_attributes
@@ -223,6 +203,12 @@ module Anyway # :nodoc:
223
203
 
224
204
  def coerce_types(mapping)
225
205
  Utils.deep_merge!(coercion_mapping, mapping)
206
+
207
+ mapping.each do |key, val|
208
+ next unless val == :boolean || (val.is_a?(::Hash) && val[:type] == :boolean)
209
+
210
+ alias_method :"#{key}?", :"#{key}"
211
+ end
226
212
  end
227
213
 
228
214
  def coercion_mapping
@@ -268,7 +254,7 @@ module Anyway # :nodoc:
268
254
  names.each do |name|
269
255
  accessors_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
270
256
  def #{name}=(val)
271
- __trace__&.record_value(val, \"#{name}\", **Tracing.current_trace_source)
257
+ __trace__&.record_value(val, "#{name}", **Tracing.current_trace_source)
272
258
  values[:#{name}] = val
273
259
  end
274
260
 
@@ -297,7 +283,7 @@ module Anyway # :nodoc:
297
283
  # - SomeModule::Config => "some_module"
298
284
  # - SomeConfig => "some"
299
285
  unless name =~ /^(\w+)(::)?Config$/
300
- raise "Couldn't infer config name, please, specify it explicitly" \
286
+ raise "Couldn't infer config name, please, specify it explicitly " \
301
287
  "via `config_name :my_config`"
302
288
  end
303
289
 
@@ -430,13 +416,18 @@ module Anyway # :nodoc:
430
416
  end
431
417
  end
432
418
 
419
+ def as_env
420
+ Env.from_hash(to_h, prefix: env_prefix)
421
+ end
422
+
433
423
  private
434
424
 
435
425
  attr_reader :values, :__trace__
436
426
 
437
427
  def validate_required_attributes!
438
428
  self.class.required_attributes.select do |name|
439
- values[name].nil? || (values[name].is_a?(String) && values[name].empty?)
429
+ val = values.dig(*name.to_s.split(".").map(&:to_sym))
430
+ val.nil? || (val.is_a?(String) && val.empty?)
440
431
  end.then do |missing|
441
432
  next if missing.empty?
442
433
  raise_validation_error "The following config parameters for `#{self.class.name}(config_name: #{self.class.config_name})` are missing or empty: #{missing.join(", ")}"
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "anyway/ext/hash"
5
+
6
+ using Anyway::Ext::Hash
7
+
8
+ module Anyway
9
+ class EJSONParser
10
+ attr_reader :bin_path
11
+
12
+ def initialize(bin_path = "ejson")
13
+ @bin_path = bin_path
14
+ end
15
+
16
+ def call(file_path)
17
+ return unless File.exist?(file_path)
18
+
19
+ raw_content = nil
20
+
21
+ stdout, stderr, status = Open3.capture3("#{bin_path} decrypt #{file_path}")
22
+
23
+ if status.success?
24
+ raw_content = JSON.parse(stdout.chomp)
25
+ else
26
+ Kernel.warn "Failed to decrypt #{file_path}: #{stderr}"
27
+ end
28
+
29
+ return unless raw_content
30
+
31
+ raw_content.deep_transform_keys do |key|
32
+ if key[0] == "_"
33
+ key[1..]
34
+ else
35
+ key
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/anyway/env.rb CHANGED
@@ -8,14 +8,31 @@ module Anyway
8
8
  using Anyway::Ext::DeepDup
9
9
  using Anyway::Ext::Hash
10
10
 
11
+ class << self
12
+ def from_hash(hash, prefix: nil, memo: {})
13
+ hash.each do |key, value|
14
+ prefix_with_key = (prefix && !prefix.empty?) ? "#{prefix}_#{key.to_s.upcase}" : key.to_s.upcase
15
+
16
+ if value.is_a?(Hash)
17
+ from_hash(value, prefix: "#{prefix_with_key}_", memo:)
18
+ else
19
+ memo[prefix_with_key] = value.to_s
20
+ end
21
+ end
22
+
23
+ memo
24
+ end
25
+ end
26
+
11
27
  include Tracing
12
28
 
13
- attr_reader :data, :traces, :type_cast
29
+ attr_reader :data, :traces, :type_cast, :env_container
14
30
 
15
- def initialize(type_cast: AutoCast)
31
+ def initialize(type_cast: AutoCast, env_container: ENV)
16
32
  @type_cast = type_cast
17
33
  @data = {}
18
34
  @traces = {}
35
+ @env_container = env_container
19
36
  end
20
37
 
21
38
  def clear
@@ -42,11 +59,11 @@ module Anyway
42
59
  private
43
60
 
44
61
  def parse_env(prefix)
45
- match_prefix = "#{prefix}_"
46
- ENV.each_pair.with_object({}) do |(key, val), data|
62
+ match_prefix = prefix.empty? ? prefix : "#{prefix}_"
63
+ env_container.each_pair.with_object({}) do |(key, val), data|
47
64
  next unless key.start_with?(match_prefix)
48
65
 
49
- path = key.sub(/^#{prefix}_/, "").downcase
66
+ path = key.sub(/^#{match_prefix}/, "").downcase
50
67
 
51
68
  paths = path.split("__")
52
69
  trace!(:env, *paths, key:) { data.bury(type_cast.call(val), *paths) }
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ module Ext
5
+ # Convert Hash with mixed array and hash values to an
6
+ # array of paths.
7
+ module FlattenNames
8
+ refine ::Array do
9
+ def flatten_names(prefix, buf)
10
+ if empty?
11
+ buf << :"#{prefix}"
12
+ return buf
13
+ end
14
+
15
+ each_with_object(buf) do |name, acc|
16
+ if name.is_a?(::Symbol)
17
+ acc << :"#{prefix}.#{name}"
18
+ else
19
+ name.flatten_names(prefix, acc)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ refine ::Hash do
26
+ def flatten_names(prefix = nil, buf = [])
27
+ each_with_object(buf) do |(k, v), acc|
28
+ parent = prefix ? "#{prefix}.#{k}" : k
29
+ v.flatten_names(parent, acc)
30
+ end
31
+ end
32
+ end
33
+
34
+ using self
35
+ end
36
+ end
37
+ end
@@ -21,11 +21,18 @@ module Anyway
21
21
 
22
22
  last_key = path.pop
23
23
  hash = path.reduce(self) do |hash, k|
24
- hash[k] = {} unless hash.key?(k)
24
+ hash[k] = {} unless hash.key?(k) && hash[k].is_a?(::Hash)
25
25
  hash[k]
26
26
  end
27
+
27
28
  hash[last_key] = val
28
29
  end
30
+
31
+ def deep_transform_keys(&block)
32
+ each_with_object({}) do |(key, value), result|
33
+ result[yield(key)] = value.is_a?(::Hash) ? value.deep_transform_keys(&block) : value
34
+ end
35
+ end
29
36
  end
30
37
 
31
38
  using self
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anyway
4
+ module Ext
5
+ # Add simple safe_constantize method to String
6
+ module StringConstantize
7
+ refine String do
8
+ def safe_constantize
9
+ names = split("::")
10
+
11
+ return nil if names.empty?
12
+
13
+ # Remove the first blank element in case of '::ClassName' notation.
14
+ names.shift if names.size > 1 && names.first.empty?
15
+
16
+ names.inject(Object) do |constant, name|
17
+ break if constant.nil?
18
+ constant.const_get(name, false) if constant.const_defined?(name, false)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "net/http"
5
+ require "json"
6
+
7
+ module Anyway
8
+ using RubyNext
9
+
10
+ module Loaders
11
+ class Doppler < Base
12
+ class RequestError < StandardError; end
13
+
14
+ class << self
15
+ attr_accessor :download_url
16
+ attr_writer :token
17
+
18
+ def token
19
+ @token || ENV["DOPPLER_TOKEN"]
20
+ end
21
+ end
22
+
23
+ self.download_url = "https://api.doppler.com/v3/configs/config/secrets/download"
24
+
25
+ def call(env_prefix:, **_options)
26
+ env_payload = parse_doppler_response(url: Doppler.download_url, token: Doppler.token)
27
+
28
+ env = ::Anyway::Env.new(type_cast: ::Anyway::NoCast, env_container: env_payload)
29
+
30
+ env.fetch_with_trace(env_prefix).then do |(conf, trace)|
31
+ Tracing.current_trace&.merge!(trace)
32
+ conf
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def parse_doppler_response(url:, token:)
39
+ response = fetch_doppler_config(url, token)
40
+
41
+ unless response.is_a?(Net::HTTPSuccess)
42
+ raise RequestError, "#{response.code} #{response.message}"
43
+ end
44
+
45
+ JSON.parse(response.read_body)
46
+ end
47
+
48
+ def fetch_doppler_config(url, token)
49
+ uri = URI.parse(url)
50
+ raise "Doppler token is required to load configuration from Doppler" if token.nil?
51
+
52
+ http = Net::HTTP.new(uri.host, uri.port)
53
+ http.use_ssl = true if uri.scheme == "https"
54
+
55
+ request = Net::HTTP::Get.new(uri)
56
+ request["Accept"] = "application/json"
57
+ request["Authorization"] = "Bearer #{token}"
58
+
59
+ http.request(request)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway/ejson_parser"
4
+
5
+ module Anyway
6
+ module Loaders
7
+ class EJSON < Base
8
+ class << self
9
+ attr_accessor :bin_path
10
+ end
11
+
12
+ self.bin_path = "ejson"
13
+
14
+ def call(name:, ejson_parser: Anyway::EJSONParser.new(EJSON.bin_path), **_options)
15
+ configs = []
16
+
17
+ rel_config_paths.each do |rel_config_path|
18
+ secrets_hash, rel_path =
19
+ extract_hash_from_rel_config_path(
20
+ ejson_parser: ejson_parser,
21
+ rel_config_path: rel_config_path
22
+ )
23
+
24
+ next unless secrets_hash
25
+
26
+ config_hash = secrets_hash[name]
27
+
28
+ next unless config_hash.is_a?(Hash)
29
+
30
+ configs <<
31
+ trace!(:ejson, path: rel_path) do
32
+ config_hash
33
+ end
34
+ end
35
+
36
+ return {} if configs.empty?
37
+
38
+ configs.inject do |result_config, next_config|
39
+ Utils.deep_merge!(result_config, next_config)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def rel_config_paths
46
+ chain = [environmental_rel_config_path]
47
+
48
+ chain << "secrets.local.ejson" if use_local?
49
+
50
+ chain
51
+ end
52
+
53
+ def environmental_rel_config_path
54
+ if Settings.current_environment
55
+ # if environment file is absent, then take data from the default one
56
+ [
57
+ "#{Settings.current_environment}/secrets.ejson",
58
+ default_rel_config_path
59
+ ]
60
+ else
61
+ default_rel_config_path
62
+ end
63
+ end
64
+
65
+ def default_rel_config_path
66
+ "secrets.ejson"
67
+ end
68
+
69
+ def extract_hash_from_rel_config_path(ejson_parser:, rel_config_path:)
70
+ rel_config_path = [rel_config_path] unless rel_config_path.is_a?(Array)
71
+
72
+ rel_config_path.each do |rel_conf_path|
73
+ rel_path = "config/#{rel_conf_path}"
74
+ abs_path = "#{Settings.app_root}/#{rel_path}"
75
+
76
+ result = ejson_parser.call(abs_path)
77
+
78
+ return [result, rel_path] if result
79
+ end
80
+
81
+ nil
82
+ end
83
+ end
84
+ end
85
+ end
@@ -53,13 +53,13 @@ module Anyway
53
53
  # the interface when no config file is present.
54
54
  begin
55
55
  if defined?(ERB)
56
- ::YAML.load(ERB.new(File.read(path)).result, aliases: true) || {} # rubocop:disable Security/YAMLLoad
56
+ ::YAML.load(ERB.new(File.read(path)).result, aliases: true) || {}
57
57
  else
58
58
  ::YAML.load_file(path, aliases: true) || {}
59
59
  end
60
60
  rescue ArgumentError
61
61
  if defined?(ERB)
62
- ::YAML.load(ERB.new(File.read(path)).result) || {} # rubocop:disable Security/YAMLLoad
62
+ ::YAML.load(ERB.new(File.read(path)).result) || {}
63
63
  else
64
64
  ::YAML.load_file(path) || {}
65
65
  end
@@ -75,3 +75,5 @@ end
75
75
  require "anyway/loaders/base"
76
76
  require "anyway/loaders/yaml"
77
77
  require "anyway/loaders/env"
78
+ require "anyway/loaders/doppler"
79
+ require "anyway/loaders/ejson"
@@ -18,6 +18,8 @@ module Anyway
18
18
 
19
19
  return unless ::Rails.root.join(val).exist?
20
20
 
21
+ return if val == autoload_static_config_path
22
+
21
23
  autoloader&.unload
22
24
 
23
25
  @autoload_static_config_path = val
@@ -80,6 +80,20 @@ module Anyway
80
80
  def default_environmental_key?
81
81
  !default_environmental_key.nil?
82
82
  end
83
+
84
+ def matching_env?(env)
85
+ return true if env.nil? || env.to_s == current_environment
86
+
87
+ if env.is_a?(::Hash)
88
+ envs = env[:except]
89
+ excluded_envs = [envs].flat_map(&:to_s)
90
+ excluded_envs.none?(current_environment)
91
+ elsif env.is_a?(::Array)
92
+ env.flat_map(&:to_s).include?(current_environment)
93
+ else
94
+ false
95
+ end
96
+ end
83
97
  end
84
98
 
85
99
  # By default, use local files only in development (that's the purpose if the local files)