dm-paperclip 2.1.4 → 2.3.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.
@@ -0,0 +1,33 @@
1
+ module Paperclip
2
+ # This module is intended as a compatability shim for the differences in
3
+ # callbacks between Rails 2.0 and Rails 2.1.
4
+ module CallbackCompatability
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ base.send(:include, InstanceMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ # The implementation of this method is taken from the Rails 1.2.6 source,
12
+ # from rails/activerecord/lib/active_record/callbacks.rb, line 192.
13
+ def define_callbacks(*args)
14
+ args.each do |method|
15
+ self.class_eval <<-"end_eval"
16
+ def self.#{method}(*callbacks, &block)
17
+ callbacks << block if block_given?
18
+ write_inheritable_array(#{method.to_sym.inspect}, callbacks)
19
+ end
20
+ end_eval
21
+ end
22
+ end
23
+ end
24
+
25
+ module InstanceMethods
26
+ # The callbacks in < 2.1 don't worry about the extra options or the
27
+ # block, so just run what we have available.
28
+ def run_callbacks(meth, opts = nil, &blk)
29
+ callback(meth)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,13 +1,13 @@
1
1
  module Paperclip
2
-
2
+
3
3
  # Defines the geometry of an image.
4
4
  class Geometry
5
5
  attr_accessor :height, :width, :modifier
6
6
 
7
7
  # Gives a Geometry representing the given height and width
8
8
  def initialize width = nil, height = nil, modifier = nil
9
- height = nil if height == ""
10
- width = nil if width == ""
9
+ height = nil if height == ''
10
+ width = nil if width == ''
11
11
  @height = (height || width).to_f
12
12
  @width = (width || height).to_f
13
13
  @modifier = modifier
@@ -17,13 +17,18 @@ module Paperclip
17
17
  # File or path.
18
18
  def self.from_file file
19
19
  file = file.path if file.respond_to? "path"
20
- parse(`#{Paperclip.path_for_command('identify')} "#{file}"`) ||
20
+ geometry = begin
21
+ Paperclip.run("identify", %Q[-format "%wx%h" "#{file}"[0]])
22
+ rescue PaperclipCommandLineError
23
+ ""
24
+ end
25
+ parse(geometry) ||
21
26
  raise(NotIdentifiedByImageMagickError.new("#{file} is not recognized by the 'identify' command."))
22
27
  end
23
28
 
24
29
  # Parses a "WxH" formatted string, where W is the width and H is the height.
25
30
  def self.parse string
26
- if match = (string && string.match(/\b(\d*)x(\d*)\b([\>\<\#\@\%^!])?/))
31
+ if match = (string && string.match(/\b(\d*)x?(\d*)\b([\>\<\#\@\%^!])?/i))
27
32
  Geometry.new(*match[1,3])
28
33
  end
29
34
  end
@@ -60,7 +65,11 @@ module Paperclip
60
65
 
61
66
  # Returns the width and height in a format suitable to be passed to Geometry.parse
62
67
  def to_s
63
- "%dx%d%s" % [width, height, modifier]
68
+ s = ""
69
+ s << width.to_i.to_s if width > 0
70
+ s << "x#{height.to_i}" if height > 0
71
+ s << modifier.to_s
72
+ s
64
73
  end
65
74
 
66
75
  # Same as to_s
@@ -68,15 +77,14 @@ module Paperclip
68
77
  to_s
69
78
  end
70
79
 
71
- # Returns the scaling and cropping geometries (in string-based ImageMagick format)
72
- # neccessary to transform this Geometry into the Geometry given. If crop is true,
73
- # then it is assumed the destination Geometry will be the exact final resolution.
74
- # In this case, the source Geometry is scaled so that an image containing the
75
- # destination Geometry would be completely filled by the source image, and any
76
- # overhanging image would be cropped. Useful for square thumbnail images. The cropping
80
+ # Returns the scaling and cropping geometries (in string-based ImageMagick format)
81
+ # neccessary to transform this Geometry into the Geometry given. If crop is true,
82
+ # then it is assumed the destination Geometry will be the exact final resolution.
83
+ # In this case, the source Geometry is scaled so that an image containing the
84
+ # destination Geometry would be completely filled by the source image, and any
85
+ # overhanging image would be cropped. Useful for square thumbnail images. The cropping
77
86
  # is weighted at the center of the Geometry.
78
87
  def transformation_to dst, crop = false
79
-
80
88
  if crop
81
89
  ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
82
90
  scale_geometry, scale = scaling(dst, ratio)
@@ -84,7 +92,7 @@ module Paperclip
84
92
  else
85
93
  scale_geometry = dst.to_s
86
94
  end
87
-
95
+
88
96
  [ scale_geometry, crop_geometry ]
89
97
  end
90
98
 
@@ -0,0 +1,123 @@
1
+ module Paperclip
2
+ # This module contains all the methods that are available for interpolation
3
+ # in paths and urls. To add your own (or override an existing one), you
4
+ # can either open this module and define it, or call the
5
+ # Paperclip.interpolates method.
6
+ module Interpolations
7
+ extend self
8
+
9
+ # Hash assignment of interpolations. Included only for compatability,
10
+ # and is not intended for normal use.
11
+ def self.[]= name, block
12
+ define_method(name, &block)
13
+ end
14
+
15
+ # Hash access of interpolations. Included only for compatability,
16
+ # and is not intended for normal use.
17
+ def self.[] name
18
+ method(name)
19
+ end
20
+
21
+ # Returns a sorted list of all interpolations.
22
+ def self.all
23
+ self.instance_methods(false).sort
24
+ end
25
+
26
+ # Perform the actual interpolation. Takes the pattern to interpolate
27
+ # and the arguments to pass, which are the attachment and style name.
28
+ def self.interpolate pattern, *args
29
+ all.reverse.inject( pattern.dup ) do |result, tag|
30
+ result.gsub(/:#{tag}/) do |match|
31
+ send( tag, *args )
32
+ end
33
+ end
34
+ end
35
+
36
+ # Returns the filename, the same way as ":basename.:extension" would.
37
+ def filename attachment, style
38
+ "#{basename(attachment, style)}.#{extension(attachment, style)}"
39
+ end
40
+
41
+ # Returns the interpolated URL. Will raise an error if the url itself
42
+ # contains ":url" to prevent infinite recursion. This interpolation
43
+ # is used in the default :path to ease default specifications.
44
+ def url attachment, style
45
+ raise InfiniteInterpolationError if attachment.options[:url].include?(":url")
46
+ attachment.url(style, false)
47
+ end
48
+
49
+ # Returns the timestamp as defined by the <attachment>_updated_at field
50
+ def timestamp attachment, style
51
+ attachment.instance_read(:updated_at).to_s
52
+ end
53
+
54
+ def web_root attachment, style
55
+ if Object.const_defined?('Merb')
56
+ merb_root(attachment, style)
57
+ elsif Object.const_defined("RAILS_ROOT")
58
+ rails_root(attachment, style)
59
+ else
60
+ ""
61
+ end
62
+ end
63
+
64
+ # Returns the RAILS_ROOT constant.
65
+ def rails_root attachment, style
66
+ Object.const_defined?('RAILS_ROOT') ? RAILS_ROOT : nil
67
+ end
68
+
69
+ # Returns the RAILS_ENV constant.
70
+ def rails_env attachment, style
71
+ Object.const_defined?('RAILS_ENV') ? RAILS_ENV : nil
72
+ end
73
+
74
+ def merb_root attachment, style
75
+ Object.const_defined?('Merb') ? Merb.root : nil
76
+ end
77
+
78
+ def merb_env attachment, style
79
+ Object.const_defined?('Merb') ? Merb.env : nil
80
+ end
81
+
82
+ # Returns the snake cased, pluralized version of the class name.
83
+ # e.g. "users" for the User class.
84
+ def class attachment, style
85
+ attachment.instance.class.to_s.snake_case.pluralize
86
+ end
87
+
88
+ # Returns the basename of the file. e.g. "file" for "file.jpg"
89
+ def basename attachment, style
90
+ attachment.original_filename.gsub(/#{File.extname(attachment.original_filename)}$/, "")
91
+ end
92
+
93
+ # Returns the extension of the file. e.g. "jpg" for "file.jpg"
94
+ # If the style has a format defined, it will return the format instead
95
+ # of the actual extension.
96
+ def extension attachment, style
97
+ ((style = attachment.styles[style]) && style[:format]) ||
98
+ File.extname(attachment.original_filename).gsub(/^\.+/, "")
99
+ end
100
+
101
+ # Returns the id of the instance.
102
+ def id attachment, style
103
+ attachment.instance.id
104
+ end
105
+
106
+ # Returns the id of the instance in a split path form. e.g. returns
107
+ # 000/001/234 for an id of 1234.
108
+ def id_partition attachment, style
109
+ ("%09d" % attachment.instance.id).scan(/\d{3}/).join("/")
110
+ end
111
+
112
+ # Returns the pluralized form of the attachment name. e.g.
113
+ # "avatars" for an attachment of :avatar
114
+ def attachment attachment, style
115
+ attachment.name.to_s.downcase.pluralize
116
+ end
117
+
118
+ # Returns the style, or the default style if nil is supplied.
119
+ def style attachment, style
120
+ style || attachment.default_style
121
+ end
122
+ end
123
+ end
@@ -25,12 +25,12 @@ module IOStream
25
25
  while self.read(in_blocks_of, buffer) do
26
26
  dstio.write(buffer)
27
27
  end
28
- dstio.rewind
28
+ dstio.rewind
29
29
  dstio
30
30
  end
31
31
  end
32
32
 
33
- class IO
33
+ class IO #:nodoc:
34
34
  include IOStream
35
35
  end
36
36
 
@@ -41,3 +41,18 @@ end
41
41
  end
42
42
  end
43
43
  end
44
+
45
+ # Corrects a bug in Windows when asking for Tempfile size.
46
+ if defined? Tempfile
47
+ class Tempfile
48
+ def size
49
+ if @tmpfile
50
+ @tmpfile.fsync
51
+ @tmpfile.flush
52
+ @tmpfile.stat.size
53
+ else
54
+ 0
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,49 @@
1
+ module Paperclip
2
+ # Paperclip processors allow you to modify attached files when they are
3
+ # attached in any way you are able. Paperclip itself uses command-line
4
+ # programs for its included Thumbnail processor, but custom processors
5
+ # are not required to follow suit.
6
+ #
7
+ # Processors are required to be defined inside the Paperclip module and
8
+ # are also required to be a subclass of Paperclip::Processor. There is
9
+ # only one method you *must* implement to properly be a subclass:
10
+ # #make, but #initialize may also be of use. Both methods accept 3
11
+ # arguments: the file that will be operated on (which is an instance of
12
+ # File), a hash of options that were defined in has_attached_file's
13
+ # style hash, and the Paperclip::Attachment itself.
14
+ #
15
+ # All #make needs to return is an instance of File (Tempfile is
16
+ # acceptable) which contains the results of the processing.
17
+ #
18
+ # See Paperclip.run for more information about using command-line
19
+ # utilities from within Processors.
20
+ class Processor
21
+ attr_accessor :file, :options, :attachment
22
+
23
+ def initialize file, options = {}, attachment = nil
24
+ @file = file
25
+ @options = options
26
+ @attachment = attachment
27
+ end
28
+
29
+ def make
30
+ end
31
+
32
+ def self.make file, options = {}, attachment = nil
33
+ new(file, options, attachment).make
34
+ end
35
+ end
36
+
37
+ # Due to how ImageMagick handles its image format conversion and how Tempfile
38
+ # handles its naming scheme, it is necessary to override how Tempfile makes
39
+ # its names so as to allow for file extensions. Idea taken from the comments
40
+ # on this blog post:
41
+ # http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions
42
+ class Tempfile < ::Tempfile
43
+ # Replaces Tempfile's +make_tmpname+ with one that honors file extensions.
44
+ def make_tmpname(basename, n)
45
+ extension = File.extname(basename)
46
+ sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n, extension)
47
+ end
48
+ end
49
+ end
@@ -8,18 +8,18 @@ module Paperclip
8
8
  # * +path+: The location of the repository of attachments on disk. This can (and, in
9
9
  # almost all cases, should) be coordinated with the value of the +url+ option to
10
10
  # allow files to be saved into a place where Apache can serve them without
11
- # hitting your app. Defaults to
11
+ # hitting your app. Defaults to
12
12
  # ":rails_root/public/:attachment/:id/:style/:basename.:extension"
13
- # By default this places the files in the app's public directory which can be served
14
- # directly. If you are using capistrano for deployment, a good idea would be to
15
- # make a symlink to the capistrano-created system directory from inside your app's
13
+ # By default this places the files in the app's public directory which can be served
14
+ # directly. If you are using capistrano for deployment, a good idea would be to
15
+ # make a symlink to the capistrano-created system directory from inside your app's
16
16
  # public directory.
17
17
  # See Paperclip::Attachment#interpolate for more information on variable interpolaton.
18
- # :path => "/var/app/attachments/:class/:id/:style/:filename"
18
+ # :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
19
19
  module Filesystem
20
20
  def self.extended base
21
21
  end
22
-
22
+
23
23
  def exists?(style = default_style)
24
24
  if original_filename
25
25
  File.exist?(path(style))
@@ -31,31 +31,40 @@ module Paperclip
31
31
  # Returns representation of the data of the file assigned to the given
32
32
  # style, in the format most representative of the current storage.
33
33
  def to_file style = default_style
34
- @queued_for_write[style] || (File.new(path(style)) if exists?(style))
34
+ @queued_for_write[style] || (File.new(path(style), 'rb') if exists?(style))
35
35
  end
36
36
  alias_method :to_io, :to_file
37
37
 
38
38
  def flush_writes #:nodoc:
39
- #logger.info("[paperclip] Writing files for #{name}")
40
39
  @queued_for_write.each do |style, file|
41
- FileUtils.mkdir_p(File.dirname(path(style)))
42
- #logger.info("[paperclip] -> #{path(style)}")
43
- result = file.stream_to(path(style))
44
40
  file.close
45
- result.close
41
+ FileUtils.mkdir_p(File.dirname(path(style)))
42
+ log("saving #{path(style)}")
43
+ FileUtils.mv(file.path, path(style))
44
+ FileUtils.chmod(0644, path(style))
46
45
  end
47
46
  @queued_for_write = {}
48
47
  end
49
48
 
50
49
  def flush_deletes #:nodoc:
51
- #logger.info("[paperclip] Deleting files for #{name}")
52
50
  @queued_for_delete.each do |path|
53
51
  begin
54
- #logger.info("[paperclip] -> #{path}")
52
+ log("deleting #{path}")
55
53
  FileUtils.rm(path) if File.exist?(path)
56
54
  rescue Errno::ENOENT => e
57
55
  # ignore file-not-found, let everything else pass
58
56
  end
57
+ begin
58
+ while(true)
59
+ path = File.dirname(path)
60
+ FileUtils.rmdir(path)
61
+ end
62
+ rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR
63
+ # Stop trying to remove parent directories
64
+ rescue SystemCallError => e
65
+ log("There was an unexpected error while deleting directories: #{e.class}")
66
+ # Ignore it
67
+ end
59
68
  end
60
69
  @queued_for_delete = []
61
70
  end
@@ -70,35 +79,48 @@ module Paperclip
70
79
  # database.yml file, so different environments can use different accounts:
71
80
  # development:
72
81
  # access_key_id: 123...
73
- # secret_access_key: 123...
82
+ # secret_access_key: 123...
74
83
  # test:
75
84
  # access_key_id: abc...
76
- # secret_access_key: abc...
85
+ # secret_access_key: abc...
77
86
  # production:
78
87
  # access_key_id: 456...
79
- # secret_access_key: 456...
88
+ # secret_access_key: 456...
80
89
  # This is not required, however, and the file may simply look like this:
81
90
  # access_key_id: 456...
82
- # secret_access_key: 456...
91
+ # secret_access_key: 456...
83
92
  # In which case, those access keys will be used in all environments. You can also
84
93
  # put your bucket name in this file, instead of adding it to the code directly.
85
- # This is useful when you want the same account but a different bucket for
94
+ # This is useful when you want the same account but a different bucket for
86
95
  # development versus production.
87
96
  # * +s3_permissions+: This is a String that should be one of the "canned" access
88
97
  # policies that S3 provides (more information can be found here:
89
98
  # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
90
99
  # The default for Paperclip is "public-read".
91
- # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
100
+ # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
92
101
  # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
93
102
  # default), and 'https' when your :s3_permissions are anything else.
103
+ # * +s3_headers+: A hash of headers such as {'Expires' => 1.year.from_now.httpdate}
94
104
  # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
95
105
  # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
96
106
  # Paperclip will attempt to create it. The bucket name will not be interpolated.
97
- # * +url+: There are two options for the S3 url. You can choose to have the bucket's name
107
+ # You can define the bucket as a Proc if you want to determine it's name at runtime.
108
+ # Paperclip will call that Proc with attachment as the only argument.
109
+ # * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
110
+ # S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
111
+ # link in the +url+ entry for more information about S3 domains and buckets.
112
+ # * +url+: There are three options for the S3 url. You can choose to have the bucket's name
98
113
  # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
114
+ # Lastly, you can specify a CNAME (which requires the CNAME to be specified as
115
+ # :s3_alias_url. You can read more about CNAMEs and S3 at
116
+ # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
99
117
  # Normally, this won't matter in the slightest and you can leave the default (which is
100
118
  # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
101
119
  # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
120
+ # NOTE: If you use a CNAME for use with CloudFront, you can NOT specify https as your
121
+ # :s3_protocol; This is *not supported* by S3/CloudFront. Finally, when using the host
122
+ # alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
123
+ # by S3.
102
124
  # * +path+: This is the key under the bucket in which the file will be stored. The
103
125
  # URL will be constructed from the bucket and the path. This is what you will want
104
126
  # to interpolate. Keys should be unique, like filenames, and despite the fact that
@@ -109,19 +131,24 @@ module Paperclip
109
131
  require 'right_aws'
110
132
  base.instance_eval do
111
133
  @s3_credentials = parse_credentials(@options[:s3_credentials])
112
- @bucket = @options[:bucket] || @s3_credentials[:bucket]
113
- @s3_options = @options[:s3_options] || {}
134
+ @bucket = @options[:bucket] || @s3_credentials[:bucket]
135
+ @bucket = @bucket.call(self) if @bucket.is_a?(Proc)
136
+ @s3_options = @options[:s3_options] || {}
114
137
  @s3_permissions = @options[:s3_permissions] || 'public-read'
115
- @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
138
+ @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
139
+ @s3_headers = @options[:s3_headers] || {}
140
+ @s3_host_alias = @options[:s3_host_alias]
116
141
  @url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
117
142
  end
118
- base.class.interpolations[:s3_path_url] = lambda do |attachment, style|
143
+ Paperclip.interpolates(:s3_alias_url) do |attachment, style|
144
+ "#{attachment.s3_protocol}://#{attachment.s3_host_alias}/#{attachment.path(style).gsub(%r{^/}, "")}"
145
+ end
146
+ Paperclip.interpolates(:s3_path_url) do |attachment, style|
119
147
  "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
120
148
  end
121
- base.class.interpolations[:s3_domain_url] = lambda do |attachment, style|
149
+ Paperclip.interpolates(:s3_domain_url) do |attachment, style|
122
150
  "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
123
151
  end
124
- #ActiveRecord::Base.logger.info("[paperclip] S3 Storage Initalized.")
125
152
  end
126
153
 
127
154
  def s3
@@ -138,11 +165,19 @@ module Paperclip
138
165
  @bucket
139
166
  end
140
167
 
168
+ def s3_host_alias
169
+ @s3_host_alias
170
+ end
171
+
141
172
  def parse_credentials creds
142
- creds = stringify_keys(find_credentials(creds))
143
- symbolize_keys((creds[Merb.env] || creds))
173
+ creds = find_credentials(creds).to_mash
174
+ if defined? Merb && Merb.respond_to?(:env)
175
+ (creds[Merb.env] || creds)
176
+ else
177
+ (creds[RAILS_ENV] || creds)
178
+ end
144
179
  end
145
-
180
+
146
181
  def exists?(style = default_style)
147
182
  s3_bucket.key(path(style)) ? true : false
148
183
  end
@@ -159,13 +194,12 @@ module Paperclip
159
194
  alias_method :to_io, :to_file
160
195
 
161
196
  def flush_writes #:nodoc:
162
- #logger.info("[paperclip] Writing files for #{name}")
163
197
  @queued_for_write.each do |style, file|
164
198
  begin
165
- #logger.info("[paperclip] -> #{path(style)}")
199
+ log("saving #{path(style)}")
166
200
  key = s3_bucket.key(path(style))
167
201
  key.data = file
168
- key.put(nil, @s3_permissions)
202
+ key.put(nil, @s3_permissions, {'Content-type' => instance_read(:content_type)}.merge(@s3_headers))
169
203
  rescue RightAws::AwsError => e
170
204
  raise
171
205
  end
@@ -174,10 +208,9 @@ module Paperclip
174
208
  end
175
209
 
176
210
  def flush_deletes #:nodoc:
177
- #logger.info("[paperclip] Writing files for #{name}")
178
211
  @queued_for_delete.each do |path|
179
212
  begin
180
- #logger.info("[paperclip] -> #{path}")
213
+ log("deleting #{path}")
181
214
  if file = s3_bucket.key(path)
182
215
  file.delete
183
216
  end
@@ -187,14 +220,14 @@ module Paperclip
187
220
  end
188
221
  @queued_for_delete = []
189
222
  end
190
-
223
+
191
224
  def find_credentials creds
192
225
  case creds
193
- when File:
226
+ when File
194
227
  YAML.load_file(creds.path)
195
- when String:
228
+ when String
196
229
  YAML.load_file(creds)
197
- when Hash:
230
+ when Hash
198
231
  creds
199
232
  else
200
233
  raise ArgumentError, "Credentials are not a path, file, or hash."
@@ -202,22 +235,6 @@ module Paperclip
202
235
  end
203
236
  private :find_credentials
204
237
 
205
- private
206
-
207
- def stringify_keys(hash)
208
- hash.inject({}) do |options, (key, value)|
209
- options[key.to_s] = value
210
- options
211
- end
212
- end
213
-
214
- def symbolize_keys(hash)
215
- hash.inject({}) do |options, (key, value)|
216
- options[key.to_sym || key] = value
217
- options
218
- end
219
- end
220
-
221
238
  end
222
239
  end
223
240
  end