activestorage 6.1.5.1 → 7.0.0.alpha1

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.

Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +129 -268
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +25 -11
  5. data/app/assets/javascripts/activestorage.esm.js +844 -0
  6. data/app/assets/javascripts/activestorage.js +257 -376
  7. data/app/controllers/active_storage/base_controller.rb +1 -10
  8. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -4
  9. data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
  10. data/app/controllers/active_storage/representations/base_controller.rb +5 -1
  11. data/app/controllers/active_storage/representations/proxy_controller.rb +6 -4
  12. data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
  13. data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
  14. data/app/controllers/concerns/active_storage/set_current.rb +3 -3
  15. data/app/controllers/concerns/active_storage/streaming.rb +65 -0
  16. data/app/javascript/activestorage/ujs.js +1 -1
  17. data/app/models/active_storage/attachment.rb +35 -2
  18. data/app/models/active_storage/blob/representable.rb +7 -5
  19. data/app/models/active_storage/blob.rb +26 -27
  20. data/app/models/active_storage/current.rb +12 -2
  21. data/app/models/active_storage/preview.rb +6 -4
  22. data/app/models/active_storage/record.rb +1 -1
  23. data/app/models/active_storage/variant.rb +6 -9
  24. data/app/models/active_storage/variant_record.rb +2 -0
  25. data/app/models/active_storage/variant_with_record.rb +9 -5
  26. data/app/models/active_storage/variation.rb +3 -3
  27. data/config/routes.rb +10 -10
  28. data/db/migrate/20170806125915_create_active_storage_tables.rb +29 -8
  29. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +15 -2
  30. data/lib/active_storage/analyzer/audio_analyzer.rb +65 -0
  31. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +39 -0
  32. data/lib/active_storage/analyzer/image_analyzer/vips.rb +49 -0
  33. data/lib/active_storage/analyzer/image_analyzer.rb +2 -30
  34. data/lib/active_storage/analyzer/video_analyzer.rb +26 -11
  35. data/lib/active_storage/analyzer.rb +8 -4
  36. data/lib/active_storage/attached/changes/create_many.rb +7 -3
  37. data/lib/active_storage/attached/changes/create_one.rb +1 -1
  38. data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
  39. data/lib/active_storage/attached/changes/delete_many.rb +1 -1
  40. data/lib/active_storage/attached/changes/delete_one.rb +1 -1
  41. data/lib/active_storage/attached/changes/detach_many.rb +18 -0
  42. data/lib/active_storage/attached/changes/detach_one.rb +24 -0
  43. data/lib/active_storage/attached/changes/purge_many.rb +27 -0
  44. data/lib/active_storage/attached/changes/purge_one.rb +27 -0
  45. data/lib/active_storage/attached/changes.rb +7 -1
  46. data/lib/active_storage/attached/many.rb +27 -15
  47. data/lib/active_storage/attached/model.rb +31 -5
  48. data/lib/active_storage/attached/one.rb +32 -27
  49. data/lib/active_storage/downloader.rb +2 -2
  50. data/lib/active_storage/engine.rb +28 -18
  51. data/lib/active_storage/fixture_set.rb +76 -0
  52. data/lib/active_storage/gem_version.rb +4 -4
  53. data/lib/active_storage/previewer/video_previewer.rb +0 -2
  54. data/lib/active_storage/previewer.rb +4 -4
  55. data/lib/active_storage/reflection.rb +12 -2
  56. data/lib/active_storage/service/azure_storage_service.rb +1 -1
  57. data/lib/active_storage/service/configurator.rb +1 -1
  58. data/lib/active_storage/service/disk_service.rb +13 -18
  59. data/lib/active_storage/service/gcs_service.rb +91 -7
  60. data/lib/active_storage/service/mirror_service.rb +1 -1
  61. data/lib/active_storage/service/registry.rb +1 -1
  62. data/lib/active_storage/service/s3_service.rb +4 -4
  63. data/lib/active_storage/service.rb +3 -3
  64. data/lib/active_storage/transformers/image_processing_transformer.rb +1 -353
  65. data/lib/active_storage/transformers/transformer.rb +1 -1
  66. data/lib/active_storage.rb +3 -4
  67. metadata +31 -23
  68. data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  gem "google-cloud-storage", "~> 1.11"
4
+ require "google/apis/iamcredentials_v1"
4
5
  require "google/cloud/storage"
5
6
 
6
7
  module ActiveStorage
7
8
  # Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
8
9
  # documentation that applies to all services.
9
10
  class Service::GCSService < Service
11
+ class MetadataServerError < ActiveStorage::Error; end
12
+ class MetadataServerNotFoundError < ActiveStorage::Error; end
13
+
10
14
  def initialize(public: false, **config)
11
15
  @config = config
12
16
  @public = public
@@ -19,7 +23,7 @@ module ActiveStorage
19
23
  # binary and attachment when the file's content type requires it. The only way to force them is to
20
24
  # store them as object's metadata.
21
25
  content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
22
- bucket.create_file(io, key, md5: checksum, content_type: content_type, content_disposition: content_disposition)
26
+ bucket.create_file(io, key, md5: checksum, cache_control: @config[:cache_control], content_type: content_type, content_disposition: content_disposition)
23
27
  rescue Google::Cloud::InvalidArgumentError
24
28
  raise ActiveStorage::IntegrityError
25
29
  end
@@ -84,7 +88,31 @@ module ActiveStorage
84
88
 
85
89
  def url_for_direct_upload(key, expires_in:, checksum:, **)
86
90
  instrument :url, key: key do |payload|
87
- generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_md5: checksum
91
+ headers = {}
92
+ version = :v2
93
+
94
+ if @config[:cache_control].present?
95
+ headers["Cache-Control"] = @config[:cache_control]
96
+ # v2 signing doesn't support non `x-goog-` headers. Only switch to v4 signing
97
+ # if necessary for back-compat; v4 limits the expiration of the URL to 7 days
98
+ # whereas v2 has no limit
99
+ version = :v4
100
+ end
101
+
102
+ args = {
103
+ content_md5: checksum,
104
+ expires: expires_in,
105
+ headers: headers,
106
+ method: "PUT",
107
+ version: version,
108
+ }
109
+
110
+ if @config[:iam]
111
+ args[:issuer] = issuer
112
+ args[:signer] = signer
113
+ end
114
+
115
+ generated_url = bucket.signed_url(key, **args)
88
116
 
89
117
  payload[:url] = generated_url
90
118
 
@@ -95,15 +123,31 @@ module ActiveStorage
95
123
  def headers_for_direct_upload(key, checksum:, filename: nil, disposition: nil, **)
96
124
  content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
97
125
 
98
- { "Content-MD5" => checksum, "Content-Disposition" => content_disposition }
126
+ headers = { "Content-MD5" => checksum, "Content-Disposition" => content_disposition }
127
+
128
+ if @config[:cache_control].present?
129
+ headers["Cache-Control"] = @config[:cache_control]
130
+ end
131
+
132
+ headers
99
133
  end
100
134
 
101
135
  private
102
136
  def private_url(key, expires_in:, filename:, content_type:, disposition:, **)
103
- file_for(key).signed_url expires: expires_in, query: {
104
- "response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
105
- "response-content-type" => content_type
137
+ args = {
138
+ expires: expires_in,
139
+ query: {
140
+ "response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
141
+ "response-content-type" => content_type
142
+ }
106
143
  }
144
+
145
+ if @config[:iam]
146
+ args[:issuer] = issuer
147
+ args[:signer] = signer
148
+ end
149
+
150
+ file_for(key).signed_url(**args)
107
151
  end
108
152
 
109
153
  def public_url(key, **)
@@ -137,7 +181,47 @@ module ActiveStorage
137
181
  end
138
182
 
139
183
  def client
140
- @client ||= Google::Cloud::Storage.new(**config.except(:bucket))
184
+ @client ||= Google::Cloud::Storage.new(**config.except(:bucket, :cache_control, :iam, :gsa_email))
185
+ end
186
+
187
+ def issuer
188
+ @issuer ||= if @config[:gsa_email]
189
+ @config[:gsa_email]
190
+ else
191
+ uri = URI.parse("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email")
192
+ http = Net::HTTP.new(uri.host, uri.port)
193
+ request = Net::HTTP::Get.new(uri.request_uri)
194
+ request["Metadata-Flavor"] = "Google"
195
+
196
+ begin
197
+ response = http.request(request)
198
+ rescue SocketError
199
+ raise MetadataServerNotFoundError
200
+ end
201
+
202
+ if response.is_a?(Net::HTTPSuccess)
203
+ response.body
204
+ else
205
+ raise MetadataServerError
206
+ end
207
+ end
208
+ end
209
+
210
+ def signer
211
+ # https://googleapis.dev/ruby/google-cloud-storage/latest/Google/Cloud/Storage/Project.html#signed_url-instance_method
212
+ lambda do |string_to_sign|
213
+ iam_client = Google::Apis::IamcredentialsV1::IAMCredentialsService.new
214
+
215
+ scopes = ["https://www.googleapis.com/auth/iam"]
216
+ iam_client.authorization = Google::Auth.get_application_default(scopes)
217
+
218
+ request = Google::Apis::IamcredentialsV1::SignBlobRequest.new(
219
+ payload: string_to_sign
220
+ )
221
+ resource = "projects/-/serviceAccounts/#{issuer}"
222
+ response = iam_client.sign_service_account_blob(resource, request)
223
+ response.signed_blob
224
+ end
141
225
  end
142
226
  end
143
227
  end
@@ -17,7 +17,7 @@ module ActiveStorage
17
17
  :url_for_direct_upload, :headers_for_direct_upload, :path_for, to: :primary
18
18
 
19
19
  # Stitch together from named services.
20
- def self.build(primary:, mirrors:, name:, configurator:, **options) #:nodoc:
20
+ def self.build(primary:, mirrors:, name:, configurator:, **options) # :nodoc:
21
21
  new(
22
22
  primary: configurator.build(primary),
23
23
  mirrors: mirrors.collect { |mirror_name| configurator.build mirror_name }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- class Service::Registry #:nodoc:
4
+ class Service::Registry # :nodoc:
5
5
  def initialize(configurations)
6
6
  @configurations = configurations.deep_symbolize_keys
7
7
  @services = {}
@@ -96,14 +96,14 @@ module ActiveStorage
96
96
  end
97
97
 
98
98
  private
99
- def private_url(key, expires_in:, filename:, disposition:, content_type:, **)
99
+ def private_url(key, expires_in:, filename:, disposition:, content_type:, **client_opts)
100
100
  object_for(key).presigned_url :get, expires_in: expires_in.to_i,
101
101
  response_content_disposition: content_disposition_with(type: disposition, filename: filename),
102
- response_content_type: content_type
102
+ response_content_type: content_type, **client_opts
103
103
  end
104
104
 
105
- def public_url(key, **)
106
- object_for(key).public_url
105
+ def public_url(key, **client_opts)
106
+ object_for(key).public_url(**client_opts)
107
107
  end
108
108
 
109
109
 
@@ -35,8 +35,8 @@ module ActiveStorage
35
35
  # can configure the service to use like this:
36
36
  #
37
37
  # ActiveStorage::Blob.service = ActiveStorage::Service.configure(
38
- # :Disk,
39
- # root: Pathname("/foo/bar/storage")
38
+ # :local,
39
+ # { local: {service: "Disk", root: Pathname("/tmp/foo/storage") } }
40
40
  # )
41
41
  class Service
42
42
  extend ActiveSupport::Autoload
@@ -57,7 +57,7 @@ module ActiveStorage
57
57
  # Passes the configurator and all of the service's config as keyword args.
58
58
  #
59
59
  # See MirrorService for an example.
60
- def build(configurator:, name:, service: nil, **service_config) #:nodoc:
60
+ def build(configurator:, name:, service: nil, **service_config) # :nodoc:
61
61
  new(**service_config).tap do |service_instance|
62
62
  service_instance.name = name
63
63
  end
@@ -13,300 +13,6 @@ module ActiveStorage
13
13
  module Transformers
14
14
  class ImageProcessingTransformer < Transformer
15
15
  private
16
- class UnsupportedImageProcessingMethod < StandardError; end
17
- class UnsupportedImageProcessingArgument < StandardError; end
18
- SUPPORTED_IMAGE_PROCESSING_METHODS = [
19
- "adaptive_blur",
20
- "adaptive_resize",
21
- "adaptive_sharpen",
22
- "adjoin",
23
- "affine",
24
- "alpha",
25
- "annotate",
26
- "antialias",
27
- "append",
28
- "apply",
29
- "attenuate",
30
- "authenticate",
31
- "auto_gamma",
32
- "auto_level",
33
- "auto_orient",
34
- "auto_threshold",
35
- "backdrop",
36
- "background",
37
- "bench",
38
- "bias",
39
- "bilateral_blur",
40
- "black_point_compensation",
41
- "black_threshold",
42
- "blend",
43
- "blue_primary",
44
- "blue_shift",
45
- "blur",
46
- "border",
47
- "bordercolor",
48
- "borderwidth",
49
- "brightness_contrast",
50
- "cache",
51
- "canny",
52
- "caption",
53
- "channel",
54
- "channel_fx",
55
- "charcoal",
56
- "chop",
57
- "clahe",
58
- "clamp",
59
- "clip",
60
- "clip_path",
61
- "clone",
62
- "clut",
63
- "coalesce",
64
- "colorize",
65
- "colormap",
66
- "color_matrix",
67
- "colors",
68
- "colorspace",
69
- "colourspace",
70
- "color_threshold",
71
- "combine",
72
- "combine_options",
73
- "comment",
74
- "compare",
75
- "complex",
76
- "compose",
77
- "composite",
78
- "compress",
79
- "connected_components",
80
- "contrast",
81
- "contrast_stretch",
82
- "convert",
83
- "convolve",
84
- "copy",
85
- "crop",
86
- "cycle",
87
- "deconstruct",
88
- "define",
89
- "delay",
90
- "delete",
91
- "density",
92
- "depth",
93
- "descend",
94
- "deskew",
95
- "despeckle",
96
- "direction",
97
- "displace",
98
- "dispose",
99
- "dissimilarity_threshold",
100
- "dissolve",
101
- "distort",
102
- "dither",
103
- "draw",
104
- "duplicate",
105
- "edge",
106
- "emboss",
107
- "encoding",
108
- "endian",
109
- "enhance",
110
- "equalize",
111
- "evaluate",
112
- "evaluate_sequence",
113
- "extent",
114
- "extract",
115
- "family",
116
- "features",
117
- "fft",
118
- "fill",
119
- "filter",
120
- "flatten",
121
- "flip",
122
- "floodfill",
123
- "flop",
124
- "font",
125
- "foreground",
126
- "format",
127
- "frame",
128
- "function",
129
- "fuzz",
130
- "fx",
131
- "gamma",
132
- "gaussian_blur",
133
- "geometry",
134
- "gravity",
135
- "grayscale",
136
- "green_primary",
137
- "hald_clut",
138
- "highlight_color",
139
- "hough_lines",
140
- "iconGeometry",
141
- "iconic",
142
- "identify",
143
- "ift",
144
- "illuminant",
145
- "immutable",
146
- "implode",
147
- "insert",
148
- "intensity",
149
- "intent",
150
- "interlace",
151
- "interline_spacing",
152
- "interpolate",
153
- "interpolative_resize",
154
- "interword_spacing",
155
- "kerning",
156
- "kmeans",
157
- "kuwahara",
158
- "label",
159
- "lat",
160
- "layers",
161
- "level",
162
- "level_colors",
163
- "limit",
164
- "limits",
165
- "linear_stretch",
166
- "linewidth",
167
- "liquid_rescale",
168
- "list",
169
- "loader",
170
- "log",
171
- "loop",
172
- "lowlight_color",
173
- "magnify",
174
- "map",
175
- "mattecolor",
176
- "median",
177
- "mean_shift",
178
- "metric",
179
- "mode",
180
- "modulate",
181
- "moments",
182
- "monitor",
183
- "monochrome",
184
- "morph",
185
- "morphology",
186
- "mosaic",
187
- "motion_blur",
188
- "name",
189
- "negate",
190
- "noise",
191
- "normalize",
192
- "opaque",
193
- "ordered_dither",
194
- "orient",
195
- "page",
196
- "paint",
197
- "pause",
198
- "perceptible",
199
- "ping",
200
- "pointsize",
201
- "polaroid",
202
- "poly",
203
- "posterize",
204
- "precision",
205
- "preview",
206
- "process",
207
- "quality",
208
- "quantize",
209
- "quiet",
210
- "radial_blur",
211
- "raise",
212
- "random_threshold",
213
- "range_threshold",
214
- "red_primary",
215
- "regard_warnings",
216
- "region",
217
- "remote",
218
- "render",
219
- "repage",
220
- "resample",
221
- "resize",
222
- "resize_to_fill",
223
- "resize_to_fit",
224
- "resize_to_limit",
225
- "resize_and_pad",
226
- "respect_parentheses",
227
- "reverse",
228
- "roll",
229
- "rotate",
230
- "sample",
231
- "sampling_factor",
232
- "saver",
233
- "scale",
234
- "scene",
235
- "screen",
236
- "seed",
237
- "segment",
238
- "selective_blur",
239
- "separate",
240
- "sepia_tone",
241
- "shade",
242
- "shadow",
243
- "shared_memory",
244
- "sharpen",
245
- "shave",
246
- "shear",
247
- "sigmoidal_contrast",
248
- "silent",
249
- "similarity_threshold",
250
- "size",
251
- "sketch",
252
- "smush",
253
- "snaps",
254
- "solarize",
255
- "sort_pixels",
256
- "sparse_color",
257
- "splice",
258
- "spread",
259
- "statistic",
260
- "stegano",
261
- "stereo",
262
- "storage_type",
263
- "stretch",
264
- "strip",
265
- "stroke",
266
- "strokewidth",
267
- "style",
268
- "subimage_search",
269
- "swap",
270
- "swirl",
271
- "synchronize",
272
- "taint",
273
- "text_font",
274
- "threshold",
275
- "thumbnail",
276
- "tile_offset",
277
- "tint",
278
- "title",
279
- "transform",
280
- "transparent",
281
- "transparent_color",
282
- "transpose",
283
- "transverse",
284
- "treedepth",
285
- "trim",
286
- "type",
287
- "undercolor",
288
- "unique_colors",
289
- "units",
290
- "unsharp",
291
- "update",
292
- "valid_image",
293
- "view",
294
- "vignette",
295
- "virtual_pixel",
296
- "visual",
297
- "watermark",
298
- "wave",
299
- "wavelet_denoise",
300
- "weight",
301
- "white_balance",
302
- "white_point",
303
- "white_threshold",
304
- "window",
305
- "window_group"
306
- ].concat(ActiveStorage.supported_image_processing_methods)
307
-
308
- UNSUPPORTED_IMAGE_PROCESSING_ARGUMENTS = ActiveStorage.unsupported_image_processing_arguments
309
-
310
16
  def process(file, format:)
311
17
  processor.
312
18
  source(file).
@@ -322,14 +28,10 @@ module ActiveStorage
322
28
 
323
29
  def operations
324
30
  transformations.each_with_object([]) do |(name, argument), list|
325
- if ActiveStorage.variant_processor == :mini_magick
326
- validate_transformation(name, argument)
327
- end
328
-
329
31
  if name.to_s == "combine_options"
330
32
  raise ArgumentError, <<~ERROR.squish
331
33
  Active Storage's ImageProcessing transformer doesn't support :combine_options,
332
- as it always generates a single ImageMagick command.
34
+ as it always generates a single command.
333
35
  ERROR
334
36
  end
335
37
 
@@ -338,60 +40,6 @@ module ActiveStorage
338
40
  end
339
41
  end
340
42
  end
341
-
342
- def validate_transformation(name, argument)
343
- method_name = name.to_s.tr("-", "_")
344
-
345
- unless SUPPORTED_IMAGE_PROCESSING_METHODS.any? { |method| method_name == method }
346
- raise UnsupportedImageProcessingMethod, <<~ERROR.squish
347
- One or more of the provided transformation methods is not supported.
348
- ERROR
349
- end
350
-
351
- if argument.present?
352
- if argument.is_a?(String) || argument.is_a?(Symbol)
353
- validate_arg_string(argument)
354
- elsif argument.is_a?(Array)
355
- validate_arg_array(argument)
356
- elsif argument.is_a?(Hash)
357
- validate_arg_hash(argument)
358
- end
359
- end
360
- end
361
-
362
- def validate_arg_string(argument)
363
- if UNSUPPORTED_IMAGE_PROCESSING_ARGUMENTS.any? { |bad_arg| argument.to_s.downcase.include?(bad_arg) }; raise UnsupportedImageProcessingArgument end
364
- end
365
-
366
- def validate_arg_array(argument)
367
- argument.each do |arg|
368
- if arg.is_a?(Integer) || arg.is_a?(Float)
369
- next
370
- elsif arg.is_a?(String) || arg.is_a?(Symbol)
371
- validate_arg_string(arg)
372
- elsif arg.is_a?(Array)
373
- validate_arg_array(arg)
374
- elsif arg.is_a?(Hash)
375
- validate_arg_hash(arg)
376
- end
377
- end
378
- end
379
-
380
- def validate_arg_hash(argument)
381
- argument.each do |key, value|
382
- validate_arg_string(key)
383
-
384
- if value.is_a?(Integer) || value.is_a?(Float)
385
- next
386
- elsif value.is_a?(String) || value.is_a?(Symbol)
387
- validate_arg_string(value)
388
- elsif value.is_a?(Array)
389
- validate_arg_array(value)
390
- elsif value.is_a?(Hash)
391
- validate_arg_hash(value)
392
- end
393
- end
394
- end
395
43
  end
396
44
  end
397
45
  end
@@ -31,7 +31,7 @@ module ActiveStorage
31
31
  private
32
32
  # Returns an open Tempfile containing a transformed image in the given +format+.
33
33
  # All subclasses implement this method.
34
- def process(file, format:) #:doc:
34
+ def process(file, format:) # :doc:
35
35
  raise NotImplementedError
36
36
  end
37
37
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # Copyright (c) 2017-2022 David Heinemeier Hansson, Basecamp
4
+ # Copyright (c) 2017-2021 David Heinemeier Hansson, Basecamp
5
5
  #
6
6
  # Permission is hereby granted, free of charge, to any person obtaining
7
7
  # a copy of this software and associated documentation files (the
@@ -37,6 +37,7 @@ module ActiveStorage
37
37
  extend ActiveSupport::Autoload
38
38
 
39
39
  autoload :Attached
40
+ autoload :FixtureSet
40
41
  autoload :Service
41
42
  autoload :Previewer
42
43
  autoload :Analyzer
@@ -58,10 +59,8 @@ module ActiveStorage
58
59
  mattr_accessor :content_types_to_serve_as_binary, default: []
59
60
  mattr_accessor :content_types_allowed_inline, default: []
60
61
 
61
- mattr_accessor :supported_image_processing_methods, default: []
62
- mattr_accessor :unsupported_image_processing_arguments
63
-
64
62
  mattr_accessor :service_urls_expire_in, default: 5.minutes
63
+ mattr_accessor :urls_expire_in
65
64
 
66
65
  mattr_accessor :routes_prefix, default: "/rails/active_storage"
67
66
  mattr_accessor :draw_routes, default: true