visor-image 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/visor +423 -0
- data/bin/visor-image +10 -0
- data/config/server.rb +14 -0
- data/lib/image/auth.rb +147 -0
- data/lib/image/cli.rb +397 -0
- data/lib/image/client.rb +490 -0
- data/lib/image/meta.rb +219 -0
- data/lib/image/routes/delete_all_images.rb +40 -0
- data/lib/image/routes/delete_image.rb +62 -0
- data/lib/image/routes/get_image.rb +78 -0
- data/lib/image/routes/get_images.rb +54 -0
- data/lib/image/routes/get_images_detail.rb +54 -0
- data/lib/image/routes/head_image.rb +51 -0
- data/lib/image/routes/post_image.rb +189 -0
- data/lib/image/routes/put_image.rb +205 -0
- data/lib/image/server.rb +307 -0
- data/lib/image/store/cumulus.rb +126 -0
- data/lib/image/store/file_system.rb +119 -0
- data/lib/image/store/hdfs.rb +149 -0
- data/lib/image/store/http.rb +78 -0
- data/lib/image/store/lunacloud.rb +126 -0
- data/lib/image/store/s3.rb +121 -0
- data/lib/image/store/store.rb +39 -0
- data/lib/image/store/walrus.rb +130 -0
- data/lib/image/version.rb +5 -0
- data/lib/visor-image.rb +30 -0
- data/spec/lib/client_spec.rb +0 -0
- data/spec/lib/meta_spec.rb +230 -0
- data/spec/lib/routes/delete_image_spec.rb +98 -0
- data/spec/lib/routes/get_image_spec.rb +78 -0
- data/spec/lib/routes/get_images_detail_spec.rb +104 -0
- data/spec/lib/routes/get_images_spec.rb +104 -0
- data/spec/lib/routes/head_image_spec.rb +51 -0
- data/spec/lib/routes/post_image_spec.rb +112 -0
- data/spec/lib/routes/put_image_spec.rb +109 -0
- data/spec/lib/server_spec.rb +62 -0
- data/spec/lib/store/cumulus_spec.rb +0 -0
- data/spec/lib/store/file_system_spec.rb +32 -0
- data/spec/lib/store/http_spec.rb +56 -0
- data/spec/lib/store/s3_spec.rb +37 -0
- data/spec/lib/store/store_spec.rb +36 -0
- data/spec/lib/store/walrus_spec.rb +0 -0
- metadata +217 -0
data/lib/image/server.rb
ADDED
@@ -0,0 +1,307 @@
|
|
1
|
+
require 'goliath'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Visor
|
6
|
+
module Image
|
7
|
+
|
8
|
+
# The VISoR Image Server. This supports all image metadata manipulation
|
9
|
+
# operations, dispatched to the VISoR Meta Server and the image files storage operations.
|
10
|
+
#
|
11
|
+
# *The Server API is a RESTful web service for image meta as follows:*
|
12
|
+
#
|
13
|
+
# HEAD /images/<id> - Returns metadata about the image with the given id
|
14
|
+
# GET /images - Returns a set of brief metadata about all public images
|
15
|
+
# GET /images/detail - Returns a set of detailed metadata about all public images
|
16
|
+
# GET /images/<id> - Returns image data and metadata for the image with the given id
|
17
|
+
# POST /images - Stores a new image data and metadata and returns the registered metadata
|
18
|
+
# PUT /images/<id> - Update image metadata and/or data for the image with the given id
|
19
|
+
# DELETE /images/<id> - Delete the metadata and data of the image with the given id
|
20
|
+
#
|
21
|
+
# The Image is multi-format, most properly it supports response encoding in JSON and XML formats.
|
22
|
+
# It will auto negotiate and encode the response metadata and error messages in the proper format,
|
23
|
+
# based on the request *Accept* header, being it 'application/json' or 'application/xml'.
|
24
|
+
# If no Accept header is provided, Image will encode and render it as JSON by default.
|
25
|
+
#
|
26
|
+
# This server routes all metadata operations to the {Visor::Meta::Server Visor Meta Server}.
|
27
|
+
#
|
28
|
+
# The server will interact with the multiple supported backend store to manage the image files.
|
29
|
+
#
|
30
|
+
# *Currently we support the following store backend and operations:*
|
31
|
+
#
|
32
|
+
# Amazon Simple Storage (s3) - GET, POST, PUT, DELETE
|
33
|
+
# Nimbus Cumulus (cumulus) - GET, POST, PUT, DELETE
|
34
|
+
# Eucalyptus Walrus (walrus) - GET, POST, PUT, DELETE
|
35
|
+
# Local Filesystem (file) - GET, POST, PUT, DELETE
|
36
|
+
# Remote HTTP (http) - GET
|
37
|
+
#
|
38
|
+
class Server < Goliath::API
|
39
|
+
|
40
|
+
# Middleware
|
41
|
+
#
|
42
|
+
# Listen at /status for a heartbeat server message status
|
43
|
+
use Goliath::Rack::Heartbeat
|
44
|
+
# Auto parse and merge body and query parameters
|
45
|
+
use Goliath::Rack::Params
|
46
|
+
# Cleanup accepted media types
|
47
|
+
use Goliath::Rack::DefaultMimeType
|
48
|
+
|
49
|
+
|
50
|
+
# Routes
|
51
|
+
#
|
52
|
+
|
53
|
+
# @method head_image
|
54
|
+
# @overload head '/image/:id'
|
55
|
+
#
|
56
|
+
# Head metadata about the image with the given id, see {Visor::Image::HeadImage}.
|
57
|
+
#
|
58
|
+
# The image metadata is promptly passed as a set of HTTP headers, as the following example:
|
59
|
+
#
|
60
|
+
# x-image-meta-_id: <id>
|
61
|
+
# x-image-meta-uri: <uri>
|
62
|
+
# x-image-meta-name: <name>
|
63
|
+
# x-image-meta-architecture: <architecture>
|
64
|
+
# x-image-meta-access: <access>
|
65
|
+
# x-image-meta-status: <status>
|
66
|
+
# x-image-meta-created_at: <timestamp>
|
67
|
+
#
|
68
|
+
# @note Undefined attributes are ignored and not included into response's header. Also
|
69
|
+
# any raised error will be passed through HTTP headers as expected.
|
70
|
+
#
|
71
|
+
# @param [String] id The wanted image _id.
|
72
|
+
#
|
73
|
+
# @example Retrieve the image metadata with id '19b39ed6-6c43-41cc-8d59-d1a1ed24ac9d':
|
74
|
+
# 'HEAD /images/19b39ed6-6c43-41cc-8d59-d1a1ed24ac9d'
|
75
|
+
#
|
76
|
+
# @return [HTTP Headers] The image data file.
|
77
|
+
#
|
78
|
+
# @raise [HTTP Error 404] If image not found.
|
79
|
+
# @raise [HTTP Error 500] On internal server error.
|
80
|
+
#
|
81
|
+
head '/images/:id', HeadImage
|
82
|
+
|
83
|
+
|
84
|
+
# @method get_all_brief
|
85
|
+
# @overload get '/images'
|
86
|
+
#
|
87
|
+
# Get brief information about all public images, see {Visor::Image::GetImages}.
|
88
|
+
#
|
89
|
+
# { "images":
|
90
|
+
# [
|
91
|
+
# {
|
92
|
+
# "_id":<_id>,
|
93
|
+
# "uri":<uri>,
|
94
|
+
# "name":<name>,
|
95
|
+
# "architecture":<architecture>,
|
96
|
+
# "type":<type>,
|
97
|
+
# "format":<format>,
|
98
|
+
# "store":<type>,
|
99
|
+
# "size":<size>,
|
100
|
+
# "created_at":<timestamp>
|
101
|
+
# },
|
102
|
+
# ...
|
103
|
+
# ]
|
104
|
+
# }
|
105
|
+
#
|
106
|
+
# The following options can be passed as query parameters, plus any other additional
|
107
|
+
# image attribute not defined in the schema.
|
108
|
+
#
|
109
|
+
# @param [String, Integer, Date] parameter The image attribute to filter results based on its value.
|
110
|
+
# @param [String] sort ("_id") The image attribute to sort results.
|
111
|
+
# @param [String] dir ("asc") The sorting order ("asc"/"desc").
|
112
|
+
#
|
113
|
+
# @example Retrieve all public images brief metadata:
|
114
|
+
# 'GET /images'
|
115
|
+
#
|
116
|
+
# @example Retrieve all public `x86_64` images brief metadata:
|
117
|
+
# 'GET /images?architecture=x86_64'
|
118
|
+
#
|
119
|
+
# @example Retrieve all public `.iso` images brief metadata, descending sorted by `size`:
|
120
|
+
# 'GET /images?format=iso&sort=size&dir=desc'
|
121
|
+
#
|
122
|
+
# @return [JSON, XML] The public images brief metadata.
|
123
|
+
#
|
124
|
+
# @raise [HTTP Error 404] If there is no public images.
|
125
|
+
# @raise [HTTP Error 500] On internal server error.
|
126
|
+
#
|
127
|
+
get '/images', GetImages
|
128
|
+
|
129
|
+
|
130
|
+
# @method get_all_detail
|
131
|
+
# @overload get '/images/detail'
|
132
|
+
#
|
133
|
+
# Get detailed information about all public images, see {Visor::Image::GetImagesDetail}.
|
134
|
+
#
|
135
|
+
# { "images":
|
136
|
+
# [
|
137
|
+
# {
|
138
|
+
# "_id":<_id>,
|
139
|
+
# "uri":<uri>,
|
140
|
+
# "name":<name>,
|
141
|
+
# "architecture":<architecture>,
|
142
|
+
# "access":<access>,
|
143
|
+
# "status":<status>,
|
144
|
+
# "size":<size>,
|
145
|
+
# "type":<type>,
|
146
|
+
# "format":<format>,
|
147
|
+
# "store":<store>,
|
148
|
+
# "created_at":<timestamp>
|
149
|
+
# "updated_at":<timestamp>,
|
150
|
+
# "kernel":<associated kernel>,
|
151
|
+
# "ramdisk":<associated ramdisk>,
|
152
|
+
# },
|
153
|
+
# ...
|
154
|
+
# ]
|
155
|
+
# }
|
156
|
+
#
|
157
|
+
# The following options can be passed as query parameters, plus any other additional
|
158
|
+
# image attribute not defined in the schema.
|
159
|
+
#
|
160
|
+
# @param [String, Integer, Date] parameter The image attribute to filter results based on its value.
|
161
|
+
# @param [String] sort ("_id") The image attribute to sort results.
|
162
|
+
# @param [String] dir ("asc") The sorting order ("asc"/"desc").
|
163
|
+
#
|
164
|
+
# @example Retrieve all public images detailed metadata:
|
165
|
+
# 'GET /images/detail'
|
166
|
+
#
|
167
|
+
# @note Querying and ordering results are made as with #get_all_detail
|
168
|
+
#
|
169
|
+
# @return [JSON, XML] The public images detailed metadata.
|
170
|
+
#
|
171
|
+
# @raise [HTTP Error 404] If there is no public images.
|
172
|
+
# @raise [HTTP Error 500] On internal server error.
|
173
|
+
#
|
174
|
+
get '/images/detail', GetImagesDetail
|
175
|
+
|
176
|
+
|
177
|
+
# @method get_image
|
178
|
+
# @overload get '/images/:id'
|
179
|
+
#
|
180
|
+
# Get image data and detailed metadata for the given id, see {Visor::Image::GetImage}.
|
181
|
+
#
|
182
|
+
# The image data file is streamed as response's body. The server will return a 200 status code,
|
183
|
+
# with a special HTTP header, indicating that the response body will be streamed in chunks
|
184
|
+
# and connection shouldn't be closed until the transfer is complete.
|
185
|
+
#
|
186
|
+
# Also, the image metadata is promptly passed as a set of HTTP headers, as the following example:
|
187
|
+
#
|
188
|
+
# x-image-meta-_id: <id>
|
189
|
+
# x-image-meta-uri: <uri>
|
190
|
+
# x-image-meta-name: <name>
|
191
|
+
# x-image-meta-architecture: <architecture>
|
192
|
+
# x-image-meta-access: <access>
|
193
|
+
# x-image-meta-status: <status>
|
194
|
+
# x-image-meta-created_at: <timestamp>
|
195
|
+
#
|
196
|
+
# @note Undefined attributes are ignored and not included into response's header.
|
197
|
+
#
|
198
|
+
# @param [String] id The wanted image _id.
|
199
|
+
#
|
200
|
+
# @example Retrieve the image with id '19b39ed6-6c43-41cc-8d59-d1a1ed24ac9d':
|
201
|
+
# 'GET /images/19b39ed6-6c43-41cc-8d59-d1a1ed24ac9d'
|
202
|
+
#
|
203
|
+
# @return [HTTP Headers] The image data file.
|
204
|
+
# @return [HTTP Body] The image data file.
|
205
|
+
#
|
206
|
+
# @raise [HTTP Error 404] If image not found.
|
207
|
+
# @raise [HTTP Error 500] On internal server error.
|
208
|
+
#
|
209
|
+
get '/images/:id', GetImage
|
210
|
+
|
211
|
+
|
212
|
+
# @method post
|
213
|
+
# @overload post '/images'
|
214
|
+
#
|
215
|
+
# Post image data and metadata and returns the registered metadata, see {Visor::Image::PostImage}.
|
216
|
+
#
|
217
|
+
# The image metadata should be encoded as HTTP headers and passed with the request, as the following example:
|
218
|
+
#
|
219
|
+
# x-image-meta-name: Ubuntu 10.10 Desktop
|
220
|
+
# x-image-meta-architecture: i386
|
221
|
+
# x-image-meta-store: s3
|
222
|
+
#
|
223
|
+
# If wanted, the image data file should be streamed through the request body.
|
224
|
+
# The server will receive that data in chunks, buffering them to a properly secured temporary
|
225
|
+
# file, avoiding buffering all the data into memory. Server will then handle the upload of that
|
226
|
+
# data to the definitive store location, cleaning then the generated temporary file.
|
227
|
+
#
|
228
|
+
# Alternatively, a *x-image-meta-location* header can be passed, if the file is already stored in some
|
229
|
+
# provided location. If this header is present, no body content can be passed in the request.
|
230
|
+
#
|
231
|
+
# @return [JSON, XML] The already created image detailed metadata.
|
232
|
+
#
|
233
|
+
# @raise [HTTP Error 400] If the image metadata validation fails.
|
234
|
+
# @raise [HTTP Error 400] If the location header is present no file content can be provided.
|
235
|
+
# @raise [HTTP Error 400] If trying to post an image file to a HTTP backend.
|
236
|
+
# @raise [HTTP Error 400] If provided store is an unsupported store backend.
|
237
|
+
# @raise [HTTP Error 404] If no image data is found at the provided location.
|
238
|
+
# @raise [HTTP Error 409] If the provided image file already exists in the backend store.
|
239
|
+
# @raise [HTTP Error 500] On internal server error.
|
240
|
+
#
|
241
|
+
post '/images', PostImage
|
242
|
+
|
243
|
+
|
244
|
+
# @method put
|
245
|
+
# @overload put '/images/:id'
|
246
|
+
#
|
247
|
+
# Put image metadata and/or data for the image with the given id, see {Visor::Image::PutImage}.
|
248
|
+
#
|
249
|
+
# The image metadata should be encoded as HTTP headers and passed with the request, as the following example:
|
250
|
+
#
|
251
|
+
# x-image-meta-name: Ubuntu 10.10 Server
|
252
|
+
# x-image-meta-architecture: x86_64
|
253
|
+
# x-image-meta-location: http://www.ubuntu.com/start-download?distro=server&bits=64&release=latest
|
254
|
+
#
|
255
|
+
# If wanted, the image data file should be streamed through the request body.
|
256
|
+
# The server will receive that data in chunks, buffering them to a properly secured temporary
|
257
|
+
# file, avoiding buffering all the data into memory. Server will then handle the upload of that
|
258
|
+
# data to the definitive store location, cleaning then the generated temporary file.
|
259
|
+
#
|
260
|
+
# Alternatively, a *x-image-meta-location* header can be passed, if the file is already stored in some
|
261
|
+
# provided location. If this header is present, no body content can be passed in the request.
|
262
|
+
#
|
263
|
+
# @note Only images with status set to 'locked' or 'error' can be updated with an image data file.
|
264
|
+
#
|
265
|
+
# @return [JSON, XML] The already updated image detailed metadata.
|
266
|
+
#
|
267
|
+
# @raise [HTTP Error 400] If the image metadata validation fails.
|
268
|
+
# @raise [HTTP Error 400] If no headers neither body found for update.
|
269
|
+
# @raise [HTTP Error 400] If the location header is present no file content can be provided.
|
270
|
+
# @raise [HTTP Error 400] If trying to post an image file to a HTTP backend.
|
271
|
+
# @raise [HTTP Error 400] If provided store is an unsupported store backend.
|
272
|
+
# @raise [HTTP Error 404] If no image data is found at the provided location.
|
273
|
+
# @raise [HTTP Error 409] If trying to assign image file to a locked or uploading image.
|
274
|
+
# @raise [HTTP Error 409] If the provided image file already exists in the backend store.
|
275
|
+
# @raise [HTTP Error 500] On internal server error.
|
276
|
+
#
|
277
|
+
put '/images/:id', PutImage
|
278
|
+
|
279
|
+
|
280
|
+
# @method delete
|
281
|
+
# @overload delete '/images/:id'
|
282
|
+
#
|
283
|
+
# Delete the metadata and data of the image with the given id, see {Visor::Image::DeleteImage}.
|
284
|
+
#
|
285
|
+
# @param [String] id The image _id to delete.
|
286
|
+
#
|
287
|
+
# @example Delete the image with id '19b39ed6-6c43-41cc-8d59-d1a1ed24ac9d':
|
288
|
+
# 'DELETE /images/19b39ed6-6c43-41cc-8d59-d1a1ed24ac9d'
|
289
|
+
#
|
290
|
+
# @return [JSON, XML] The already deleted image detailed metadata.
|
291
|
+
#
|
292
|
+
# @raise [HTTP Error 404] If image meta or data not found.
|
293
|
+
# @raise [HTTP Error 403] If user does not have permission to manipulate the image file.
|
294
|
+
# @raise [HTTP Error 500] On internal server error.
|
295
|
+
#
|
296
|
+
delete '/images/:id', DeleteImage
|
297
|
+
|
298
|
+
delete '/all', DeleteAllImages
|
299
|
+
|
300
|
+
# Not Found
|
301
|
+
not_found('/') do
|
302
|
+
run Proc.new { |env| [404, {}, {code: 404, message: "Invalid operation or path."}.to_json] }
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
end
|
307
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 's3restful'
|
3
|
+
require 'em-synchrony'
|
4
|
+
require 'em-synchrony/em-http'
|
5
|
+
|
6
|
+
module Visor
|
7
|
+
module Image
|
8
|
+
module Store
|
9
|
+
|
10
|
+
# The Nimbus Cumulus (Cumulus) backend store.
|
11
|
+
#
|
12
|
+
# This class handles the management of image files located in the Cumulus storage system,
|
13
|
+
# based on a URI like *cumulus://<access_key>:<secret_key>@<host>:<port>/<bucket>/<image>*.
|
14
|
+
#
|
15
|
+
class Cumulus
|
16
|
+
include Visor::Common::Exception
|
17
|
+
|
18
|
+
attr_accessor :uri, :config, :access_key, :secret_key, :bucket, :file, :host, :port
|
19
|
+
|
20
|
+
# Initializes a new Cumulus store client object. Cumulus credentials are loaded from the URI,
|
21
|
+
# on GET and DELETE operations, or from the configuration file for POST and PUT operation.
|
22
|
+
#
|
23
|
+
# @param [String] uri The URI of the file location.
|
24
|
+
# @param config [Hash] A set of configurations for the wanted store, loaded from
|
25
|
+
# VISoR configuration file.
|
26
|
+
#
|
27
|
+
# @return [Object] An instantiated Cumulus store object ready to use.
|
28
|
+
#
|
29
|
+
def initialize(uri, config)
|
30
|
+
@uri = URI(uri)
|
31
|
+
@config = config[:cumulus]
|
32
|
+
|
33
|
+
if @uri.scheme
|
34
|
+
@access_key = @uri.user
|
35
|
+
@secret_key = @uri.password
|
36
|
+
@bucket = @uri.path.split('/')[1]
|
37
|
+
@file = @uri.path.split('/')[2]
|
38
|
+
@host = @uri.host
|
39
|
+
@port = @uri.port
|
40
|
+
else
|
41
|
+
@access_key = @config[:access_key]
|
42
|
+
@secret_key = @config[:secret_key]
|
43
|
+
@bucket = @config[:bucket]
|
44
|
+
@host = @config[:host]
|
45
|
+
@port = @config[:port]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a Happening library S3 Cumulus compatible connection object.
|
50
|
+
#
|
51
|
+
# @return [Happening::S3::Item] A new Cumulus connection object.
|
52
|
+
#
|
53
|
+
def connection
|
54
|
+
S3restful::S3::Item.new(bucket, file, server: host, port: port, protocol: 'http',
|
55
|
+
aws_access_key_id: access_key, aws_secret_access_key: secret_key)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the image file to clients, streamed in chunks.
|
59
|
+
#
|
60
|
+
# @return [Object] Yields the file, a chunk at time.
|
61
|
+
#
|
62
|
+
def get
|
63
|
+
s3 = connection.aget
|
64
|
+
finish = proc { yield nil }
|
65
|
+
|
66
|
+
s3.stream { |chunk| yield chunk }
|
67
|
+
s3.callback &finish
|
68
|
+
s3.errback &finish
|
69
|
+
end
|
70
|
+
|
71
|
+
# Saves the image file to the its final destination, based on the temporary file
|
72
|
+
# created by the server at data reception time.
|
73
|
+
#
|
74
|
+
# @param [String] id The image id.
|
75
|
+
# @param [File] tmp_file The temporary file descriptor.
|
76
|
+
# @param [String] format The image file format.
|
77
|
+
#
|
78
|
+
# @return [String, Integer] The generated file location URI and image file size.
|
79
|
+
#
|
80
|
+
# @raise [Duplicated] If the image file already exists.
|
81
|
+
#
|
82
|
+
def save(id, tmp_file, format)
|
83
|
+
@file = "#{id}.#{format}"
|
84
|
+
uri = "cumulus://#{access_key}:#{secret_key}@#{host}:#{port}/#{bucket}/#{file}"
|
85
|
+
size = tmp_file.size
|
86
|
+
|
87
|
+
raise Duplicated, "The image file #{fp} already exists" if file_exists?(false)
|
88
|
+
STDERR.puts "COPYING!!"
|
89
|
+
|
90
|
+
connection.store tmp_file.path
|
91
|
+
|
92
|
+
[uri, size]
|
93
|
+
end
|
94
|
+
|
95
|
+
# Deletes the image file from its location.
|
96
|
+
#
|
97
|
+
# @raise [NotFound] If the image file was not found.
|
98
|
+
#
|
99
|
+
def delete
|
100
|
+
connection.delete
|
101
|
+
end
|
102
|
+
|
103
|
+
# Check if the image file exists.
|
104
|
+
#
|
105
|
+
# @param [True, False] raise_exc (true) If it should raise exception or return
|
106
|
+
# true/false whether the file exists or not.
|
107
|
+
#
|
108
|
+
# @return [True, False] If raise_exc is false, return true/false whether the
|
109
|
+
# file exists or not.
|
110
|
+
#
|
111
|
+
# @raise [NotFound] If the image file was not found.
|
112
|
+
#
|
113
|
+
def file_exists?(raise_exc=true)
|
114
|
+
exist = nil
|
115
|
+
error = proc { exist = false }
|
116
|
+
success = proc { |res| exist = true if res.response_header.status == 200 }
|
117
|
+
|
118
|
+
connection.head(on_error: error, on_success: success)
|
119
|
+
raise NotFound, "No image file found at #{uri}" if raise_exc && !exist
|
120
|
+
exist
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Visor
|
4
|
+
module Image
|
5
|
+
module Store
|
6
|
+
|
7
|
+
# The FileSystem backend store.
|
8
|
+
#
|
9
|
+
# This class handles the management of image files located in the local FileSystem,
|
10
|
+
# based on a URI like *file:///path/to/my_image.format*.
|
11
|
+
#
|
12
|
+
class FileSystem
|
13
|
+
include Visor::Common::Exception
|
14
|
+
|
15
|
+
# Size of the chunk to stream the files out.
|
16
|
+
CHUNKSIZE = 65536
|
17
|
+
|
18
|
+
attr_accessor :uri, :fp, :config
|
19
|
+
|
20
|
+
# Initializes a new FileSystem store client object.
|
21
|
+
#
|
22
|
+
# @param [String] uri The URI of the file location.
|
23
|
+
# @param config [Hash] A set of configurations for the wanted store, loaded from
|
24
|
+
# VISoR configuration file.
|
25
|
+
#
|
26
|
+
# @return [Object] An instantiated FileSystem store object ready to use.
|
27
|
+
#
|
28
|
+
def initialize(uri, config)
|
29
|
+
@uri = URI(uri)
|
30
|
+
@fp = @uri.path
|
31
|
+
@config = config[:file]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the image file to clients, streamed in chunks.
|
35
|
+
#
|
36
|
+
# @return [Object] Yields the file, a chunk at time.
|
37
|
+
#
|
38
|
+
def get
|
39
|
+
file_exists?
|
40
|
+
open(fp, "rb") do |file|
|
41
|
+
yield file.read(CHUNKSIZE) until file.eof?
|
42
|
+
yield nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Saves the image file to the its final destination, based on the temporary file
|
47
|
+
# created by the server at data reception time.
|
48
|
+
#
|
49
|
+
# @param [String] id The image id.
|
50
|
+
# @param [File] tmp_file The temporary file descriptor.
|
51
|
+
# @param [String] format The image file format.
|
52
|
+
#
|
53
|
+
# @return [String, Integer] The generated file location URI and image file size.
|
54
|
+
#
|
55
|
+
# @raise [Duplicated] If the image file already exists.
|
56
|
+
#
|
57
|
+
def save(id, tmp_file, format)
|
58
|
+
dir = File.expand_path config[:directory]
|
59
|
+
file = "#{id}.#{format}"
|
60
|
+
fp = File.join(dir, file)
|
61
|
+
uri = "file://#{fp}"
|
62
|
+
size = tmp_file.size
|
63
|
+
|
64
|
+
FileUtils.mkpath(dir) unless Dir.exists?(dir)
|
65
|
+
raise Duplicated, "The image file #{fp} already exists" if File.exists?(fp)
|
66
|
+
STDERR.puts "Copying image tempfile #{tmp_file.path} to definitive #{fp}"
|
67
|
+
|
68
|
+
tmp = File.open(tmp_file.path, "rb")
|
69
|
+
new = File.open(fp, "wb")
|
70
|
+
|
71
|
+
each_chunk(tmp, CHUNKSIZE) do |chunk|
|
72
|
+
new << chunk
|
73
|
+
end
|
74
|
+
|
75
|
+
[uri, size]
|
76
|
+
end
|
77
|
+
|
78
|
+
# Deletes the image file to from its location.
|
79
|
+
#
|
80
|
+
# @raise [Forbidden] If user does not have permission to manipulate the image file.
|
81
|
+
# @raise [NotFound] If the image file was not found.
|
82
|
+
#
|
83
|
+
def delete
|
84
|
+
file_exists?
|
85
|
+
begin
|
86
|
+
File.delete(fp)
|
87
|
+
rescue => e
|
88
|
+
raise Forbidden, "Error while trying to delete image file #{fp}: #{e.message}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Check if the image file exists.
|
93
|
+
#
|
94
|
+
# @raise [NotFound] If the image file was not found.
|
95
|
+
#
|
96
|
+
def file_exists?
|
97
|
+
raise NotFound, "No image file found at #{fp}" unless File.exists?(fp)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# Iterates over the image file yielding a chunk per reactor tick.
|
103
|
+
#
|
104
|
+
# @return [Object] Yielded image file chunk.
|
105
|
+
#
|
106
|
+
def each_chunk(file, chunk_size=CHUNKSIZE)
|
107
|
+
handler = lambda do
|
108
|
+
unless file.eof?
|
109
|
+
yield file.read(chunk_size)
|
110
|
+
EM.next_tick &handler
|
111
|
+
end
|
112
|
+
end
|
113
|
+
EM.next_tick &handler
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|