activestorage 6.0.6.1 → 6.1.7.6
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activestorage might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +230 -167
- data/MIT-LICENSE +1 -1
- data/README.md +35 -3
- data/app/controllers/active_storage/base_controller.rb +11 -0
- data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
- data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +2 -2
- data/app/controllers/active_storage/direct_uploads_controller.rb +1 -1
- data/app/controllers/active_storage/disk_controller.rb +8 -20
- data/app/controllers/active_storage/representations/base_controller.rb +14 -0
- data/app/controllers/active_storage/representations/proxy_controller.rb +13 -0
- data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +2 -4
- data/app/controllers/concerns/active_storage/file_server.rb +18 -0
- data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
- data/app/controllers/concerns/active_storage/set_current.rb +2 -2
- data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
- data/app/jobs/active_storage/mirror_job.rb +15 -0
- data/app/models/active_storage/attachment.rb +19 -11
- data/app/models/active_storage/blob/analyzable.rb +6 -2
- data/app/models/active_storage/blob/identifiable.rb +7 -6
- data/app/models/active_storage/blob/representable.rb +34 -4
- data/app/models/active_storage/blob.rb +122 -57
- data/app/models/active_storage/preview.rb +31 -10
- data/app/models/active_storage/record.rb +7 -0
- data/app/models/active_storage/variant.rb +31 -44
- data/app/models/active_storage/variant_record.rb +8 -0
- data/app/models/active_storage/variant_with_record.rb +54 -0
- data/app/models/active_storage/variation.rb +26 -21
- data/config/routes.rb +58 -8
- data/db/migrate/20170806125915_create_active_storage_tables.rb +30 -9
- data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +21 -0
- data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +26 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +3 -0
- data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +14 -3
- data/lib/active_storage/analyzer.rb +6 -0
- data/lib/active_storage/attached/changes/create_many.rb +1 -0
- data/lib/active_storage/attached/changes/create_one.rb +17 -4
- data/lib/active_storage/attached/many.rb +4 -3
- data/lib/active_storage/attached/model.rb +67 -14
- data/lib/active_storage/attached/one.rb +4 -3
- data/lib/active_storage/engine.rb +41 -43
- data/lib/active_storage/errors.rb +3 -0
- data/lib/active_storage/gem_version.rb +3 -3
- data/lib/active_storage/log_subscriber.rb +6 -0
- data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +2 -2
- data/lib/active_storage/previewer/video_previewer.rb +5 -3
- data/lib/active_storage/previewer.rb +13 -3
- data/lib/active_storage/service/azure_storage_service.rb +40 -35
- data/lib/active_storage/service/configurator.rb +3 -1
- data/lib/active_storage/service/disk_service.rb +36 -31
- data/lib/active_storage/service/gcs_service.rb +18 -16
- data/lib/active_storage/service/mirror_service.rb +31 -7
- data/lib/active_storage/service/registry.rb +32 -0
- data/lib/active_storage/service/s3_service.rb +51 -23
- data/lib/active_storage/service.rb +35 -7
- data/lib/active_storage/transformers/image_processing_transformer.rb +21 -308
- data/lib/active_storage/transformers/transformer.rb +0 -3
- data/lib/active_storage.rb +301 -7
- data/lib/tasks/activestorage.rake +5 -1
- metadata +54 -17
- data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +0 -9
- data/lib/active_storage/downloading.rb +0 -47
- data/lib/active_storage/transformers/mini_magick_transformer.rb +0 -38
@@ -5,4 +5,15 @@ class ActiveStorage::BaseController < ActionController::Base
|
|
5
5
|
include ActiveStorage::SetCurrent
|
6
6
|
|
7
7
|
protect_from_forgery with: :exception
|
8
|
+
|
9
|
+
self.etag_with_template_digest = false
|
10
|
+
|
11
|
+
private
|
12
|
+
def stream(blob)
|
13
|
+
blob.download do |chunk|
|
14
|
+
response.stream.write chunk
|
15
|
+
end
|
16
|
+
ensure
|
17
|
+
response.stream.close
|
18
|
+
end
|
8
19
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Proxy files through application. This avoids having a redirect and makes files easier to cache.
|
4
|
+
class ActiveStorage::Blobs::ProxyController < ActiveStorage::BaseController
|
5
|
+
include ActiveStorage::SetBlob
|
6
|
+
include ActiveStorage::SetHeaders
|
7
|
+
|
8
|
+
def show
|
9
|
+
http_cache_forever public: true do
|
10
|
+
set_content_headers_from @blob
|
11
|
+
stream @blob
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -4,11 +4,11 @@
|
|
4
4
|
# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
|
5
5
|
# security-through-obscurity factor of the signed blob references, you'll need to implement your own
|
6
6
|
# authenticated redirection controller.
|
7
|
-
class ActiveStorage::
|
7
|
+
class ActiveStorage::Blobs::RedirectController < ActiveStorage::BaseController
|
8
8
|
include ActiveStorage::SetBlob
|
9
9
|
|
10
10
|
def show
|
11
11
|
expires_in ActiveStorage.service_urls_expire_in
|
12
|
-
redirect_to @blob.
|
12
|
+
redirect_to @blob.url(disposition: params[:disposition])
|
13
13
|
end
|
14
14
|
end
|
@@ -11,7 +11,7 @@ class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
|
|
11
11
|
|
12
12
|
private
|
13
13
|
def blob_args
|
14
|
-
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, :
|
14
|
+
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys
|
15
15
|
end
|
16
16
|
|
17
17
|
def direct_upload_json(blob)
|
@@ -5,11 +5,13 @@
|
|
5
5
|
# Always go through the BlobsController, or your own authenticated controller, rather than directly
|
6
6
|
# to the service URL.
|
7
7
|
class ActiveStorage::DiskController < ActiveStorage::BaseController
|
8
|
+
include ActiveStorage::FileServer
|
9
|
+
|
8
10
|
skip_forgery_protection
|
9
11
|
|
10
12
|
def show
|
11
13
|
if key = decode_verified_key
|
12
|
-
serve_file
|
14
|
+
serve_file named_disk_service(key[:service_name]).path_for(key[:key]), content_type: key[:content_type], disposition: key[:disposition]
|
13
15
|
else
|
14
16
|
head :not_found
|
15
17
|
end
|
@@ -20,7 +22,7 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
|
|
20
22
|
def update
|
21
23
|
if token = decode_verified_token
|
22
24
|
if acceptable_content?(token)
|
23
|
-
|
25
|
+
named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
|
24
26
|
else
|
25
27
|
head :unprocessable_entity
|
26
28
|
end
|
@@ -32,30 +34,16 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
|
|
32
34
|
end
|
33
35
|
|
34
36
|
private
|
35
|
-
def
|
36
|
-
ActiveStorage::Blob.
|
37
|
+
def named_disk_service(name)
|
38
|
+
ActiveStorage::Blob.services.fetch(name) do
|
39
|
+
ActiveStorage::Blob.service
|
40
|
+
end
|
37
41
|
end
|
38
42
|
|
39
|
-
|
40
43
|
def decode_verified_key
|
41
44
|
ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
|
42
45
|
end
|
43
46
|
|
44
|
-
def serve_file(path, content_type:, disposition:)
|
45
|
-
Rack::File.new(nil).serving(request, path).tap do |(status, headers, body)|
|
46
|
-
self.status = status
|
47
|
-
self.response_body = body
|
48
|
-
|
49
|
-
headers.each do |name, value|
|
50
|
-
response.headers[name] = value
|
51
|
-
end
|
52
|
-
|
53
|
-
response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
|
54
|
-
response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
|
59
47
|
def decode_verified_token
|
60
48
|
ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
|
61
49
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveStorage::Representations::BaseController < ActiveStorage::BaseController #:nodoc:
|
4
|
+
include ActiveStorage::SetBlob
|
5
|
+
|
6
|
+
before_action :set_representation
|
7
|
+
|
8
|
+
private
|
9
|
+
def set_representation
|
10
|
+
@representation = @blob.representation(params[:variation_key]).processed
|
11
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
12
|
+
head :not_found
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Proxy files through application. This avoids having a redirect and makes files easier to cache.
|
4
|
+
class ActiveStorage::Representations::ProxyController < ActiveStorage::Representations::BaseController
|
5
|
+
include ActiveStorage::SetHeaders
|
6
|
+
|
7
|
+
def show
|
8
|
+
http_cache_forever public: true do
|
9
|
+
set_content_headers_from @representation.image
|
10
|
+
stream @representation
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -4,11 +4,9 @@
|
|
4
4
|
# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
|
5
5
|
# security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own
|
6
6
|
# authenticated redirection controller.
|
7
|
-
class ActiveStorage::
|
8
|
-
include ActiveStorage::SetBlob
|
9
|
-
|
7
|
+
class ActiveStorage::Representations::RedirectController < ActiveStorage::Representations::BaseController
|
10
8
|
def show
|
11
9
|
expires_in ActiveStorage.service_urls_expire_in
|
12
|
-
redirect_to @
|
10
|
+
redirect_to @representation.url(disposition: params[:disposition])
|
13
11
|
end
|
14
12
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage::FileServer # :nodoc:
|
4
|
+
private
|
5
|
+
def serve_file(path, content_type:, disposition:)
|
6
|
+
Rack::File.new(nil).serving(request, path).tap do |(status, headers, body)|
|
7
|
+
self.status = status
|
8
|
+
self.response_body = body
|
9
|
+
|
10
|
+
headers.each do |name, value|
|
11
|
+
response.headers[name] = value
|
12
|
+
end
|
13
|
+
|
14
|
+
response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
|
15
|
+
response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -9,7 +9,7 @@ module ActiveStorage::SetBlob #:nodoc:
|
|
9
9
|
|
10
10
|
private
|
11
11
|
def set_blob
|
12
|
-
@blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id])
|
12
|
+
@blob = ActiveStorage::Blob.find_signed!(params[:signed_blob_id] || params[:signed_id])
|
13
13
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
14
14
|
head :not_found
|
15
15
|
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Sets the <tt>ActiveStorage::Current.host</tt> attribute, which the disk service uses to generate URLs.
|
4
|
-
# Include this concern in custom controllers that call ActiveStorage::Blob#
|
5
|
-
# ActiveStorage::Variant#
|
4
|
+
# Include this concern in custom controllers that call ActiveStorage::Blob#url,
|
5
|
+
# ActiveStorage::Variant#url, or ActiveStorage::Preview#url so the disk service can
|
6
6
|
# generate URLs using the same host, protocol, and base path as the current request.
|
7
7
|
module ActiveStorage::SetCurrent
|
8
8
|
extend ActiveSupport::Concern
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage::SetHeaders #:nodoc:
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
private
|
7
|
+
def set_content_headers_from(blob)
|
8
|
+
response.headers["Content-Type"] = blob.content_type_for_serving
|
9
|
+
response.headers["Content-Disposition"] = ActionDispatch::Http::ContentDisposition.format \
|
10
|
+
disposition: blob.forced_disposition_for_serving || params[:disposition] || "inline", filename: blob.filename.sanitized
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/object/try"
|
4
|
+
|
5
|
+
# Provides asynchronous mirroring of directly-uploaded blobs.
|
6
|
+
class ActiveStorage::MirrorJob < ActiveStorage::BaseJob
|
7
|
+
queue_as { ActiveStorage.queues[:mirror] }
|
8
|
+
|
9
|
+
discard_on ActiveStorage::FileNotFoundError
|
10
|
+
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer
|
11
|
+
|
12
|
+
def perform(key, checksum:)
|
13
|
+
ActiveStorage::Blob.service.try(:mirror, key, checksum: checksum)
|
14
|
+
end
|
15
|
+
end
|
@@ -5,45 +5,53 @@ require "active_support/core_ext/module/delegation"
|
|
5
5
|
# Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
|
6
6
|
# but it is possible to associate many different records with the same blob. A foreign-key constraint
|
7
7
|
# on the attachments table prevents blobs from being purged if they’re still attached to any records.
|
8
|
-
|
8
|
+
#
|
9
|
+
# Attachments also have access to all methods from {ActiveStorage::Blob}[rdoc-ref:ActiveStorage::Blob].
|
10
|
+
class ActiveStorage::Attachment < ActiveStorage::Record
|
9
11
|
self.table_name = "active_storage_attachments"
|
10
12
|
|
11
13
|
belongs_to :record, polymorphic: true, touch: true
|
12
|
-
belongs_to :blob, class_name: "ActiveStorage::Blob"
|
14
|
+
belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true
|
13
15
|
|
14
16
|
delegate_missing_to :blob
|
17
|
+
delegate :signed_id, to: :blob
|
15
18
|
|
16
|
-
after_create_commit :
|
19
|
+
after_create_commit :mirror_blob_later, :analyze_blob_later
|
17
20
|
after_destroy_commit :purge_dependent_blob_later
|
18
21
|
|
19
22
|
# Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
|
20
23
|
def purge
|
21
|
-
|
24
|
+
transaction do
|
25
|
+
delete
|
26
|
+
record&.touch
|
27
|
+
end
|
22
28
|
blob&.purge
|
23
29
|
end
|
24
30
|
|
25
31
|
# Deletes the attachment and {enqueues a background job}[rdoc-ref:ActiveStorage::Blob#purge_later] to purge the blob.
|
26
32
|
def purge_later
|
27
|
-
|
33
|
+
transaction do
|
34
|
+
delete
|
35
|
+
record&.touch
|
36
|
+
end
|
28
37
|
blob&.purge_later
|
29
38
|
end
|
30
39
|
|
31
40
|
private
|
32
|
-
def identify_blob
|
33
|
-
blob.identify
|
34
|
-
end
|
35
|
-
|
36
41
|
def analyze_blob_later
|
37
42
|
blob.analyze_later unless blob.analyzed?
|
38
43
|
end
|
39
44
|
|
45
|
+
def mirror_blob_later
|
46
|
+
blob.mirror_later
|
47
|
+
end
|
48
|
+
|
40
49
|
def purge_dependent_blob_later
|
41
50
|
blob&.purge_later if dependent == :purge_later
|
42
51
|
end
|
43
52
|
|
44
|
-
|
45
53
|
def dependent
|
46
|
-
record.attachment_reflections[name]&.options
|
54
|
+
record.attachment_reflections[name]&.options&.fetch(:dependent, nil)
|
47
55
|
end
|
48
56
|
end
|
49
57
|
|
@@ -29,12 +29,16 @@ module ActiveStorage::Blob::Analyzable
|
|
29
29
|
update! metadata: metadata.merge(extract_metadata_via_analyzer)
|
30
30
|
end
|
31
31
|
|
32
|
-
# Enqueues an ActiveStorage::AnalyzeJob which calls #analyze.
|
32
|
+
# Enqueues an ActiveStorage::AnalyzeJob which calls #analyze, or calls #analyze inline based on analyzer class configuration.
|
33
33
|
#
|
34
34
|
# This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob
|
35
35
|
# again (e.g. if you add a new analyzer or modify an existing one).
|
36
36
|
def analyze_later
|
37
|
-
|
37
|
+
if analyzer_class.analyze_later?
|
38
|
+
ActiveStorage::AnalyzeJob.perform_later(self)
|
39
|
+
else
|
40
|
+
analyze
|
41
|
+
end
|
38
42
|
end
|
39
43
|
|
40
44
|
# Returns true if the blob has been analyzed.
|
@@ -2,9 +2,14 @@
|
|
2
2
|
|
3
3
|
module ActiveStorage::Blob::Identifiable
|
4
4
|
def identify
|
5
|
+
identify_without_saving
|
6
|
+
save!
|
7
|
+
end
|
8
|
+
|
9
|
+
def identify_without_saving
|
5
10
|
unless identified?
|
6
|
-
|
7
|
-
|
11
|
+
self.content_type = identify_content_type
|
12
|
+
self.identified = true
|
8
13
|
end
|
9
14
|
end
|
10
15
|
|
@@ -24,8 +29,4 @@ module ActiveStorage::Blob::Identifiable
|
|
24
29
|
""
|
25
30
|
end
|
26
31
|
end
|
27
|
-
|
28
|
-
def update_service_metadata
|
29
|
-
service.update_metadata key, **service_metadata if service_metadata.any?
|
30
|
-
end
|
31
32
|
end
|
@@ -1,16 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "mini_mime"
|
4
|
+
|
3
5
|
module ActiveStorage::Blob::Representable
|
4
6
|
extend ActiveSupport::Concern
|
5
7
|
|
6
8
|
included do
|
9
|
+
has_many :variant_records, class_name: "ActiveStorage::VariantRecord", dependent: false
|
10
|
+
before_destroy { variant_records.destroy_all if ActiveStorage.track_variants }
|
11
|
+
|
7
12
|
has_one_attached :preview_image
|
8
13
|
end
|
9
14
|
|
10
15
|
# Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
|
11
16
|
# files, and it allows any image to be transformed for size, colors, and the like. Example:
|
12
17
|
#
|
13
|
-
# avatar.variant(resize_to_limit: [100, 100]).processed.
|
18
|
+
# avatar.variant(resize_to_limit: [100, 100]).processed.url
|
14
19
|
#
|
15
20
|
# This will create and process a variant of the avatar blob that's constrained to a height and width of 100px.
|
16
21
|
# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
|
@@ -27,7 +32,7 @@ module ActiveStorage::Blob::Representable
|
|
27
32
|
# variable, call ActiveStorage::Blob#variable?.
|
28
33
|
def variant(transformations)
|
29
34
|
if variable?
|
30
|
-
|
35
|
+
variant_class.new(self, ActiveStorage::Variation.wrap(transformations).default_to(default_variant_transformations))
|
31
36
|
else
|
32
37
|
raise ActiveStorage::InvariableError
|
33
38
|
end
|
@@ -43,7 +48,7 @@ module ActiveStorage::Blob::Representable
|
|
43
48
|
# from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer
|
44
49
|
# extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document.
|
45
50
|
#
|
46
|
-
# blob.preview(resize_to_limit: [100, 100]).processed.
|
51
|
+
# blob.preview(resize_to_limit: [100, 100]).processed.url
|
47
52
|
#
|
48
53
|
# Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand.
|
49
54
|
# Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s
|
@@ -69,7 +74,7 @@ module ActiveStorage::Blob::Representable
|
|
69
74
|
|
70
75
|
# Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob.
|
71
76
|
#
|
72
|
-
# blob.representation(resize_to_limit: [100, 100]).processed.
|
77
|
+
# blob.representation(resize_to_limit: [100, 100]).processed.url
|
73
78
|
#
|
74
79
|
# Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call
|
75
80
|
# ActiveStorage::Blob#representable? to determine whether a blob is representable.
|
@@ -90,4 +95,29 @@ module ActiveStorage::Blob::Representable
|
|
90
95
|
def representable?
|
91
96
|
variable? || previewable?
|
92
97
|
end
|
98
|
+
|
99
|
+
private
|
100
|
+
def default_variant_transformations
|
101
|
+
{ format: default_variant_format }
|
102
|
+
end
|
103
|
+
|
104
|
+
def default_variant_format
|
105
|
+
if web_image?
|
106
|
+
format || :png
|
107
|
+
else
|
108
|
+
:png
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def format
|
113
|
+
if filename.extension.present? && MiniMime.lookup_by_extension(filename.extension)&.content_type == content_type
|
114
|
+
filename.extension
|
115
|
+
else
|
116
|
+
MiniMime.lookup_by_content_type(content_type)&.extension
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def variant_class
|
121
|
+
ActiveStorage.track_variants ? ActiveStorage::VariantWithRecord : ActiveStorage::Variant
|
122
|
+
end
|
93
123
|
end
|