Skip to content

Commit 24741e9

Browse files
authored
Add LocalClient for filesystem-based blobstore (#4980)
1 parent 13d9fc2 commit 24741e9

File tree

12 files changed

+858
-32
lines changed

12 files changed

+858
-32
lines changed

config/cloud_controller.yml

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -175,36 +175,32 @@ resource_pool:
175175
maximum_size: 42
176176
minimum_size: 1
177177
resource_directory_key: "spec-cc-resources"
178-
fog_connection:
179-
blobstore_timeout: 5
180-
provider: "Local"
178+
blobstore_type: local-temp-storage
179+
fog_connection: {}
181180
fog_aws_storage_options: {}
182181
fog_gcp_storage_options: {}
183182

184183
packages:
185184
app_package_directory_key: "cc-packages"
186185
max_package_size: 42
187186
max_valid_packages_stored: 42
188-
fog_connection:
189-
blobstore_timeout: 5
190-
provider: "Local"
187+
blobstore_type: local-temp-storage
188+
fog_connection: {}
191189
fog_aws_storage_options: {}
192190
fog_gcp_storage_options: {}
193191

194192
droplets:
195193
droplet_directory_key: cc-droplets
196194
max_staged_droplets_stored: 42
197-
fog_connection:
198-
blobstore_timeout: 5
199-
provider: "Local"
195+
blobstore_type: local-temp-storage
196+
fog_connection: {}
200197
fog_aws_storage_options: {}
201198
fog_gcp_storage_options: {}
202199

203200
buildpacks:
204201
buildpack_directory_key: cc-buildpacks
205-
fog_connection:
206-
blobstore_timeout: 5
207-
provider: "Local"
202+
blobstore_type: local-temp-storage
203+
fog_connection: {}
208204
fog_aws_storage_options: {}
209205
fog_gcp_storage_options: {}
210206

lib/cloud_controller/blobstore/client_provider.rb

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'cloud_controller/blobstore/fog/fog_client'
44
require 'cloud_controller/blobstore/error_handling_client'
55
require 'cloud_controller/blobstore/webdav/dav_client'
6+
require 'cloud_controller/blobstore/local/local_client'
67
require 'cloud_controller/blobstore/safe_delete_client'
78
require 'cloud_controller/blobstore/storage_cli/storage_cli_client'
89
require 'google/apis/errors'
@@ -11,11 +12,15 @@ module CloudController
1112
module Blobstore
1213
class ClientProvider
1314
def self.provide(options:, directory_key:, root_dir: nil, resource_type: nil)
14-
if options[:blobstore_type].blank? || (options[:blobstore_type] == 'fog')
15-
provide_fog(options, directory_key, root_dir)
16-
elsif options[:blobstore_type] == 'storage-cli'
17-
# storage-cli is an experimental feature and not yet fully implemented. !!! DO NOT USE IN PRODUCTION !!!
15+
case options[:blobstore_type]
16+
when 'local'
17+
provide_local(options, directory_key, root_dir, use_temp_storage: false)
18+
when 'local-temp-storage'
19+
provide_local(options, directory_key, root_dir, use_temp_storage: true)
20+
when 'storage-cli'
1821
provide_storage_cli(options, directory_key, root_dir, resource_type)
22+
when 'fog', nil, ''
23+
provide_fog(options, directory_key, root_dir)
1924
else
2025
provide_webdav(options, directory_key, root_dir)
2126
end
@@ -54,6 +59,23 @@ def provide_fog(options, directory_key, root_dir)
5459
Client.new(ErrorHandlingClient.new(SafeDeleteClient.new(retryable_client, root_dir)))
5560
end
5661

62+
def provide_local(options, directory_key, root_dir, use_temp_storage:)
63+
client = LocalClient.new(
64+
directory_key: directory_key,
65+
base_path: options[:local_blobstore_path],
66+
root_dir: root_dir,
67+
min_size: options[:minimum_size],
68+
max_size: options[:maximum_size],
69+
use_temp_storage: use_temp_storage
70+
)
71+
72+
logger = Steno.logger('cc.blobstore.local_client')
73+
errors = [StandardError]
74+
retryable_client = RetryableClient.new(client:, errors:, logger:)
75+
76+
Client.new(SafeDeleteClient.new(retryable_client, root_dir))
77+
end
78+
5779
def provide_webdav(options, directory_key, root_dir)
5880
client = DavClient.build(
5981
options.fetch(:webdav_config),
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
require 'cloud_controller/blobstore/blob'
2+
require 'openssl'
3+
4+
module CloudController
5+
module Blobstore
6+
class LocalBlob < Blob
7+
attr_reader :key
8+
9+
def initialize(key:, file_path:)
10+
@key = key
11+
@file_path = file_path
12+
end
13+
14+
def internal_download_url
15+
nil
16+
end
17+
18+
def public_download_url
19+
nil
20+
end
21+
22+
def local_path
23+
@file_path
24+
end
25+
26+
def attributes(*keys)
27+
@attributes ||= begin
28+
stat = File.stat(@file_path)
29+
{
30+
etag: OpenSSL::Digest::MD5.file(@file_path).hexdigest,
31+
last_modified: stat.mtime.httpdate,
32+
content_length: stat.size.to_s,
33+
created_at: stat.ctime
34+
}
35+
end
36+
37+
return @attributes if keys.empty?
38+
39+
@attributes.slice(*keys)
40+
end
41+
end
42+
end
43+
end
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
require 'cloud_controller/blobstore/base_client'
2+
require 'cloud_controller/blobstore/errors'
3+
require 'cloud_controller/blobstore/local/local_blob'
4+
require 'fileutils'
5+
require 'digest'
6+
7+
module CloudController
8+
module Blobstore
9+
class LocalClient < BaseClient
10+
attr_reader :root_dir
11+
12+
def initialize(
13+
directory_key:,
14+
base_path:,
15+
root_dir: nil,
16+
min_size: nil,
17+
max_size: nil,
18+
use_temp_storage: false
19+
)
20+
@directory_key = directory_key
21+
@use_temp_storage = use_temp_storage
22+
@root_dir = root_dir
23+
@min_size = min_size || 0
24+
@max_size = max_size
25+
26+
setup_storage_path(base_path)
27+
end
28+
29+
def local?
30+
true
31+
end
32+
33+
def exists?(key)
34+
File.exist?(file_path(key))
35+
end
36+
37+
def download_from_blobstore(source_key, destination_path, mode: nil)
38+
FileUtils.mkdir_p(File.dirname(destination_path))
39+
FileUtils.cp(file_path(source_key), destination_path)
40+
File.chmod(mode, destination_path) if mode
41+
rescue Errno::ENOENT
42+
raise FileNotFound.new("Could not find object '#{source_key}'")
43+
end
44+
45+
def cp_to_blobstore(source_path, destination_key)
46+
start = Time.now.utc
47+
log_entry = 'blobstore.cp-skip'
48+
49+
logger.info('blobstore.cp-start', destination_key: destination_key, source_path: source_path, bucket: @directory_key)
50+
51+
size = File.size(source_path)
52+
if within_limits?(size)
53+
destination = file_path(destination_key)
54+
FileUtils.mkdir_p(File.dirname(destination))
55+
FileUtils.cp(source_path, destination)
56+
log_entry = 'blobstore.cp-finish'
57+
end
58+
59+
duration = Time.now.utc - start
60+
logger.info(log_entry, destination_key: destination_key, duration_seconds: duration, size: size)
61+
rescue Errno::ENOENT => e
62+
raise FileNotFound.new("Could not find source file '#{source_path}': #{e.message}")
63+
end
64+
65+
def cp_file_between_keys(source_key, destination_key)
66+
source = file_path(source_key)
67+
destination = file_path(destination_key)
68+
69+
raise FileNotFound.new("Could not find object '#{source_key}'") unless File.exist?(source)
70+
71+
FileUtils.mkdir_p(File.dirname(destination))
72+
FileUtils.cp(source, destination)
73+
end
74+
75+
def delete(key)
76+
path = file_path(key)
77+
FileUtils.rm_f(path)
78+
cleanup_empty_parent_directories(path)
79+
end
80+
81+
def blob(key)
82+
path = file_path(key)
83+
return unless File.exist?(path)
84+
85+
LocalBlob.new(key: partitioned_key(key), file_path: path)
86+
end
87+
88+
def delete_blob(blob)
89+
path = File.join(@base_path, blob.key)
90+
FileUtils.rm_f(path)
91+
cleanup_empty_parent_directories(path)
92+
end
93+
94+
def delete_all(_=nil)
95+
FileUtils.rm_rf(@base_path)
96+
FileUtils.mkdir_p(@base_path)
97+
end
98+
99+
def delete_all_in_path(path)
100+
dir = File.join(@base_path, path)
101+
FileUtils.rm_rf(dir) if File.directory?(dir)
102+
end
103+
104+
def files_for(prefix, _ignored_directory_prefixes=[])
105+
pattern = File.join(@base_path, prefix, '**', '*')
106+
Enumerator.new do |yielder|
107+
Dir.glob(pattern).each do |file_path|
108+
next unless File.file?(file_path)
109+
110+
relative_path = file_path.sub("#{@base_path}/", '')
111+
yielder << LocalBlob.new(key: relative_path, file_path: file_path)
112+
end
113+
end
114+
end
115+
116+
def ensure_bucket_exists
117+
FileUtils.mkdir_p(@base_path)
118+
end
119+
120+
private
121+
122+
def setup_storage_path(base_path)
123+
if use_temp_storage?
124+
@base_path = Dir.mktmpdir(['cc-blobstore-', "-#{@directory_key}"])
125+
logger.info('storage-mode', mode: 'temp', directory_key: @directory_key, path: @base_path)
126+
register_cleanup_hook
127+
else
128+
raise ArgumentError.new('local_blobstore_path is required for persistent storage') if base_path.nil?
129+
130+
@base_path = File.join(base_path, @directory_key)
131+
FileUtils.mkdir_p(@base_path)
132+
logger.info('storage-mode', mode: 'persistent', directory_key: @directory_key, path: @base_path)
133+
end
134+
end
135+
136+
def file_path(key)
137+
File.join(@base_path, partitioned_key(key))
138+
end
139+
140+
def use_temp_storage?
141+
@use_temp_storage
142+
end
143+
144+
def register_cleanup_hook
145+
# Register cleanup handler for temp storage mode
146+
at_exit do
147+
cleanup_temp_storage
148+
end
149+
end
150+
151+
def cleanup_temp_storage
152+
return unless use_temp_storage? && @base_path && File.directory?(@base_path)
153+
154+
logger.info('temp-storage-cleanup', directory_key: @directory_key, path: @base_path)
155+
FileUtils.rm_rf(@base_path)
156+
rescue StandardError => e
157+
logger.error('temp-storage-cleanup-failed', error: e.message, path: @base_path)
158+
end
159+
160+
def logger
161+
@logger ||= Steno.logger('cc.blobstore.local_client')
162+
end
163+
164+
def cleanup_empty_parent_directories(path)
165+
dir = File.dirname(path)
166+
# Walk up the directory tree, removing empty directories until we hit the base path
167+
while dir != @base_path && dir.start_with?(@base_path)
168+
break unless File.directory?(dir)
169+
break unless Dir.empty?(dir)
170+
171+
FileUtils.rmdir(dir)
172+
dir = File.dirname(dir)
173+
end
174+
end
175+
end
176+
end
177+
end

lib/cloud_controller/config_schemas/api_schema.rb

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,37 +211,53 @@ class ApiSchema < VCAP::Config
211211
maximum_size: Integer,
212212
minimum_size: Integer,
213213
resource_directory_key: String,
214+
optional(:blobstore_type) => String,
215+
optional(:local_blobstore_path) => String,
214216
fog_connection: Hash,
215217
optional(:connection_config) => Hash,
216218
fog_aws_storage_options: Hash,
217-
fog_gcp_storage_options: Hash
219+
fog_gcp_storage_options: Hash,
220+
optional(:webdav_config) => Hash,
221+
optional(:cdn) => Hash
218222
},
219223

220224
buildpacks: {
221225
buildpack_directory_key: String,
226+
optional(:blobstore_type) => String,
227+
optional(:local_blobstore_path) => String,
222228
fog_connection: Hash,
223229
optional(:connection_config) => Hash,
224230
fog_aws_storage_options: Hash,
225-
fog_gcp_storage_options: Hash
231+
fog_gcp_storage_options: Hash,
232+
optional(:webdav_config) => Hash,
233+
optional(:cdn) => Hash
226234
},
227235

228236
packages: {
229237
max_package_size: Integer,
230238
max_valid_packages_stored: Integer,
231239
app_package_directory_key: String,
240+
optional(:blobstore_type) => String,
241+
optional(:local_blobstore_path) => String,
232242
fog_connection: Hash,
233243
optional(:connection_config) => Hash,
234244
fog_aws_storage_options: Hash,
235-
fog_gcp_storage_options: Hash
245+
fog_gcp_storage_options: Hash,
246+
optional(:webdav_config) => Hash,
247+
optional(:cdn) => Hash
236248
},
237249

238250
droplets: {
239251
droplet_directory_key: String,
240252
max_staged_droplets_stored: Integer,
253+
optional(:blobstore_type) => String,
254+
optional(:local_blobstore_path) => String,
241255
fog_connection: Hash,
242256
optional(:connection_config) => Hash,
243257
fog_aws_storage_options: Hash,
244-
fog_gcp_storage_options: Hash
258+
fog_gcp_storage_options: Hash,
259+
optional(:webdav_config) => Hash,
260+
optional(:cdn) => Hash
245261
},
246262

247263
db_encryption_key: enum(String, NilClass),

lib/cloud_controller/config_schemas/blobstore_benchmarks_schema.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ class BlobstoreBenchmarksSchema < VCAP::Config
99
blobstore_type: String,
1010
optional(:blobstore_provider) => String,
1111

12+
optional(:local_blobstore_path) => String,
1213
optional(:connection_config) => Hash,
1314
optional(:fog_connection) => Hash,
15+
optional(:webdav_config) => Hash,
16+
optional(:cdn) => Hash,
1417

1518
fog_aws_storage_options: Hash,
1619
fog_gcp_storage_options: Hash,

0 commit comments

Comments
 (0)