Class: Parse::File
Overview
The default MIME type for all files is image/jpeg. This can be default
can be changed by setting a value to Parse::File.default_mime_type.
This class represents a Parse file pointer. Parse::File has helper
methods to upload Parse files directly to Parse and manage file
associations with your classes.
Defined Under Namespace
Classes: SignedUrlError, UntrustedHostError
Constant Summary collapse
- LEGACY_FILE_RX =
Regular expression that matches the old legacy Parse hosted file name
/^tfss-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}-/- ATTRIBUTES =
The default attributes in a Parse File hash. Matches the Parse Server file-pointer wire format
{__type, name, url}. Thekeyfield onParse::Fileis in-memory only (not persisted to Parse Server because the server normalizes embedded file pointers and strips unknown fields); see #key. { __type: :string, name: :string, url: :string }.freeze
- SIGNATURE_QUERY_PARAMS =
Query-string parameter names that mark a URL as a presigned / signed URL — i.e. one whose POSSESSION grants temporary capability. Used by url_signature_param? (the detection predicate behind the URL normalization point) and exposed publicly so downstream apps wiring custom strict-mode checks can iterate the same list the SDK does. Detection is case-insensitive.
%w[ X-Amz-Signature X-Amz-Credential X-Amz-Security-Token AWSAccessKeyId Key-Pair-Id ].freeze
- DEFAULT_ALLOWED_REMOTE_PORTS =
Restrictive port allowlist for Parse::File URL fetches. By default only the standard HTTP/HTTPS ports are permitted. Operators may extend +Parse::File.allowed_remote_ports+ for legitimate non-standard CDN ports.
[80, 443, 8080, 8443].freeze
Constants inherited from Model
Model::CLASS_AUDIENCE, Model::CLASS_INSTALLATION, Model::CLASS_JOB_SCHEDULE, Model::CLASS_JOB_STATUS, Model::CLASS_PRODUCT, Model::CLASS_PUSH_STATUS, Model::CLASS_ROLE, Model::CLASS_SCHEMA, Model::CLASS_SESSION, Model::CLASS_USER, Model::ID, Model::KEY_CLASS_NAME, Model::KEY_CREATED_AT, Model::KEY_OBJECT_ID, Model::KEY_UPDATED_AT, Model::OBJECT_ID, Model::TYPE_ACL, Model::TYPE_BYTES, Model::TYPE_DATE, Model::TYPE_FIELD, Model::TYPE_FILE, Model::TYPE_GEOPOINT, Model::TYPE_NUMBER, Model::TYPE_OBJECT, Model::TYPE_POINTER, Model::TYPE_POLYGON, Model::TYPE_RELATION
Class Attribute Summary collapse
- .allowed_remote_hosts ⇒ Object
- .allowed_remote_ports ⇒ Object
-
.default_mime_type ⇒ String
The default mime type for created instances.
-
.force_ssl ⇒ Boolean
When set to true, it will make all calls to File#url.
- .max_remote_size ⇒ Object
- .remote_timeout ⇒ Object
- .signed_url_policy ⇒ Object
- .trusted_url_hosts ⇒ Object
- .untrusted_url_policy ⇒ Object
Instance Attribute Summary collapse
-
#contents ⇒ Object
The contents of the file.
-
#mime_type ⇒ String
The mime-type of the file whe.
-
#name ⇒ String
The name of the file including extension (if any).
-
#presigned_url ⇒ String?
readonly
The last signed URL the SDK saw for this file's location.
-
#presigned_url_expires_at ⇒ Time?
readonly
The expiry time (UTC) parsed from the most recent presigned URL the SDK saw, computed from the URL's own query parameters (
X-Amz-Date+X-Amz-Expiresfor SigV4,Expiresfor SigV2 / CloudFront).
Class Method Summary collapse
-
.basename(file_name, suffix = nil) ⇒ String
A proxy method for ::File.basename.
-
.cloudfront_signed_param_names ⇒ Array<Regexp>
CloudFront-signed-URL parameter names (
Signature,Policy,Expires). -
.create(url) ⇒ Parse::File
This creates a new Parse File Object with from a URL, saves it and returns it.
-
.filter_parameter_names ⇒ Array<Regexp>
Parameter names operators should add to
Rails.application.config.filter_parametersso presigned-URL query params are scrubbed from request logs by Rails itself. -
.log_filter ⇒ Regexp
Regex that matches any HTTP(S) URL carrying an unambiguously AWS-style signed-URL parameter — SigV4 (
X-Amz-*), legacy SigV2 (AWSAccessKeyId), or CloudFront (Key-Pair-Id). -
.log_filter_strict ⇒ Regexp
Stricter variant of File.log_filter that ALSO matches the JSON-encoded query separator (
\u0026for&). - .parse_class ⇒ Model::TYPE_FILE
-
.parse_presigned_expiry(url) ⇒ Time?
Parse the expiry time (UTC) of a presigned URL directly from its query parameters — the TTL is whatever the issuer chose, NEVER hardcoded SDK-side.
-
.strip_query(url) ⇒ String?
Strip the query string (everything from the first
?) from a URL. -
.url_signature_param?(url_string) ⇒ Boolean
True when the URL's query string carries any known signed-URL parameter from SIGNATURE_QUERY_PARAMS.
Instance Method Summary collapse
-
#==(u) ⇒ Boolean
Two files are equal if they have the same url.
- #attributes ⇒ Hash
-
#attributes=(h) ⇒ Object
Allows mass assignment from a Parse JSON hash.
-
#initialize(name, contents = nil, mime_type = nil) ⇒ File
constructor
The initializer to create a new file supports different inputs.
- #parse_class ⇒ Model::TYPE_FILE (also: #__type)
-
#parse_hosted_file? ⇒ Boolean
True if this file is hosted by Parse's servers.
-
#presigned_url_valid?(buffer: 60) ⇒ Boolean
True when #presigned_url is set and not yet expired (with an optional safety buffer so callers can refetch before the URL actually expires server-side).
-
#save(session_token: nil, use_master_key: nil) ⇒ Boolean
Save the file by uploading it to Parse and creating a file pointer.
-
#saved? ⇒ Boolean
A File object is considered saved when
@urland@nameare both present and@namematches the basename of@url's path component. -
#to_s ⇒ String
The url.
-
#url ⇒ String
Returns the url string for this Parse::File pointer.
-
#url=(value) ⇒ Object
Assign the file's URL.
Methods inherited from Model
#dirty?, find_class, same_parse_class?
Methods included from Client::Connectable
Constructor Details
#initialize(name, contents = nil, mime_type = nil) ⇒ File
The initializer to create a new file supports different inputs. If the first paramter is a string which starts with 'http', we then download the content of the file (and use the detected mime-type) to set the content and mime_type fields. If the first parameter is a hash, we assume it might be the Parse File hash format which contains url and name fields only. If the first paramter is a Parse::File, then we copy fields over Otherwise, creating a new file requires a name, the actual contents (usually from a File.open("local.jpg").read ) and the mime-type
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 |
# File 'lib/parse/model/file.rb', line 659 def initialize(name, contents = nil, mime_type = nil) mime_type ||= Parse::File.default_mime_type if name.is_a?(String) && name.start_with?("http") #could be url string file = Parse::File.safe_open_url(name) @contents = file.read @name = File.basename file.base_uri.to_s @mime_type = file.content_type elsif name.is_a?(Hash) self.attributes = name elsif name.is_a?(::File) @contents = contents || name.read @name = File.basename name.to_path elsif name.is_a?(Parse::File) @name = name.name # Route through the single URL normalization point so the copy # gets the same strip + stash treatment as a caller-side # `url=`. Preserve the source's presigned-URL stash # post-normalization (normalize resets them; carrying the # source's values across keeps the copy semantically # equivalent for view-render use cases). normalize_and_store_url(name.url) @presigned_url = name.presigned_url if name.presigned_url @presigned_url_expires_at = name.presigned_url_expires_at if name.presigned_url_expires_at else @name = name @contents = contents end if @name.blank? raise ArgumentError, "Invalid Parse::File initialization with name '#{@name}'" end @mime_type ||= mime_type end |
Class Attribute Details
.allowed_remote_hosts ⇒ Object
185 186 187 |
# File 'lib/parse/model/file.rb', line 185 def allowed_remote_hosts @allowed_remote_hosts ||= [] end |
.allowed_remote_ports ⇒ Object
253 254 255 |
# File 'lib/parse/model/file.rb', line 253 def allowed_remote_ports @allowed_remote_ports ||= DEFAULT_ALLOWED_REMOTE_PORTS.dup end |
.default_mime_type ⇒ String
Returns The default mime type for created instances. Default: 'image/jpeg'.
157 158 159 |
# File 'lib/parse/model/file.rb', line 157 def default_mime_type @default_mime_type ||= "image/jpeg" end |
.force_ssl ⇒ Boolean
Returns When set to true, it will make all calls to File#url.
162 163 164 |
# File 'lib/parse/model/file.rb', line 162 def force_ssl @force_ssl ||= false end |
.max_remote_size ⇒ Object
169 170 171 |
# File 'lib/parse/model/file.rb', line 169 def max_remote_size @max_remote_size ||= DEFAULT_MAX_REMOTE_SIZE end |
.remote_timeout ⇒ Object
175 176 177 |
# File 'lib/parse/model/file.rb', line 175 def remote_timeout @remote_timeout ||= DEFAULT_REMOTE_TIMEOUT end |
.signed_url_policy ⇒ Object
229 230 231 |
# File 'lib/parse/model/file.rb', line 229 def signed_url_policy @signed_url_policy ||= :strip end |
.trusted_url_hosts ⇒ Object
205 206 207 |
# File 'lib/parse/model/file.rb', line 205 def trusted_url_hosts @trusted_url_hosts ||= ["files.parsetfss.com"] end |
.untrusted_url_policy ⇒ Object
247 248 249 |
# File 'lib/parse/model/file.rb', line 247 def untrusted_url_policy @untrusted_url_policy ||= :warn end |
Instance Attribute Details
#contents ⇒ Object
Returns the contents of the file.
133 134 135 |
# File 'lib/parse/model/file.rb', line 133 def contents @contents end |
#mime_type ⇒ String
Returns the mime-type of the file whe.
136 137 138 |
# File 'lib/parse/model/file.rb', line 136 def mime_type @mime_type end |
#name ⇒ String
Returns the name of the file including extension (if any).
97 98 99 |
# File 'lib/parse/model/file.rb', line 97 def name @name end |
#presigned_url ⇒ String? (readonly)
Returns the last signed URL the SDK saw for this
file's location. Populated by the URL normalization point
(#normalize_and_store_url) whenever an incoming URL carries a
recognized signed-URL query parameter. Distinct from @url
(which is always the bare canonical URL — see rev 3 D1). The
expiry of this URL is in #presigned_url_expires_at; callers
should consult that before handing the URL to a client.
770 771 772 |
# File 'lib/parse/model/file.rb', line 770 def presigned_url @presigned_url end |
#presigned_url_expires_at ⇒ Time? (readonly)
Returns the expiry time (UTC) parsed from the most
recent presigned URL the SDK saw, computed from the URL's own
query parameters (X-Amz-Date + X-Amz-Expires for SigV4,
Expires for SigV2 / CloudFront). The TTL is never
hardcoded; whatever Parse Server's S3FilesAdapter (or whoever
issued the URL) chose is what the SDK uses.
778 779 780 |
# File 'lib/parse/model/file.rb', line 778 def presigned_url_expires_at @presigned_url_expires_at end |
Class Method Details
.basename(file_name, suffix = nil) ⇒ String
A proxy method for ::File.basename
940 941 942 943 944 945 946 |
# File 'lib/parse/model/file.rb', line 940 def self.basename(file_name, suffix = nil) if suffix.nil? ::File.basename(file_name) else ::File.basename(file_name, suffix) end end |
.cloudfront_signed_param_names ⇒ Array<Regexp>
CloudFront-signed-URL parameter names (Signature, Policy,
Expires). Opt-in extension to filter_parameter_names for
apps that proxy CloudFront-signed URLs through Rails params.
Out of scope: CloudFront signed cookies
(CloudFront-Policy, CloudFront-Signature,
CloudFront-Key-Pair-Id set as HTTP cookies rather than
query parameters) are a separate auth mechanism — Rails
parameter filtering does not see cookies, and the SDK
does not provide a separate cookie-filter list. Apps using
CloudFront signed cookies must wire their own protection
via ActionDispatch::Cookies::Middleware filters.
WARNING: these names collide with legitimate app params —
policy (privacy_policy, policy_id), signature (DocuSign /
webhook signatures), expires (any cache-control style field).
Append only when the operator has confirmed no such collision
exists in the app's request surface.
404 405 406 407 408 409 410 |
# File 'lib/parse/model/file.rb', line 404 def cloudfront_signed_param_names @cloudfront_signed_param_names ||= [ /\ASignature\z/i, /\APolicy\z/i, /\AExpires\z/i, ].freeze end |
.create(url) ⇒ Parse::File
This creates a new Parse File Object with from a URL, saves it and returns it
697 698 699 700 701 702 |
# File 'lib/parse/model/file.rb', line 697 def self.create(url) url = url.url if url.is_a?(Parse::File) file = self.new(url) file.save file end |
.filter_parameter_names ⇒ Array<Regexp>
Parameter names operators should add to
Rails.application.config.filter_parameters so presigned-URL
query params are scrubbed from request logs by Rails itself.
Defaults are AWS-prefixed only (X-Amz-*, AWSAccessKeyId,
Key-Pair-Id) so the list never over-redacts a Rails app's
privacy_policy / e-signature / policy_id form fields. For
CloudFront-heavy deployments that need bare Signature /
Policy / Expires matched as well, append
cloudfront_signed_param_names.
376 377 378 379 380 381 382 |
# File 'lib/parse/model/file.rb', line 376 def filter_parameter_names @filter_parameter_names ||= [ /\AX-Amz-/i, /\AAWSAccessKeyId\z/i, /\AKey-Pair-Id\z/i, ].freeze end |
.log_filter ⇒ Regexp
Regex that matches any HTTP(S) URL carrying an unambiguously
AWS-style signed-URL parameter — SigV4 (X-Amz-*), legacy
SigV2 (AWSAccessKeyId), or CloudFront (Key-Pair-Id).
Designed to be plugged into log scrubbers / lograge /
semantic_logger filters so accidental
Rails.logger.info(file_url) calls do not leak short-TTL
download credentials into log aggregators.
Bare Signature= and Policy= are NOT matched on their own —
they collide with too many unrelated app conventions (webhook
signatures, privacy_policy fields). CloudFront URLs always
carry Key-Pair-Id alongside Signature / Policy, so the
Key-Pair-Id match catches the whole URL substring.
This pattern matches plain-text URLs (& as the literal
query separator). For JSON-encoded log payloads — where &
is serialized as \u0026, common in Sentry / Honeybadger /
Rollbar event bodies — use log_filter_strict which accepts
both forms.
Out of scope: CloudFront signed cookies
(CloudFront-Policy, CloudFront-Signature,
CloudFront-Key-Pair-Id set as HTTP cookies rather than
query parameters) are a separate auth mechanism and the SDK
does not provide leak detection for them. Apps using
CloudFront signed cookies must scrub their own cookie
logging.
Log lines wrapped at fixed widths that split the URL mid-querystring will silently bypass either regex; scrub before line-wrapping.
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
# File 'lib/parse/model/file.rb', line 301 def log_filter @log_filter ||= %r{ https?://[^\s'"<>]+ # URL prefix [?&] # query separator (?: X-Amz-Signature | X-Amz-Credential | X-Amz-Security-Token | X-Amz-Algorithm | X-Amz-Date | X-Amz-Expires | X-Amz-SignedHeaders | AWSAccessKeyId | Key-Pair-Id ) =[^&\s'"<>]+ # signature value (?:&[^\s'"<>]*)? # trailing params }xi.freeze end |
.log_filter_strict ⇒ Regexp
Stricter variant of log_filter that ALSO matches the
JSON-encoded query separator (\u0026 for &). Use this
when scrubbing error-reporter event bodies (Sentry,
Honeybadger, Rollbar, Bugsnag) where the URL string has been
JSON-encoded once and the literal & appears as \u0026.
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 |
# File 'lib/parse/model/file.rb', line 337 def log_filter_strict # URL prefix excludes the backslash so it doesn't greedily # consume the `\u0026` sequence in JSON-encoded payloads. # Separator and trailing-params clauses both accept either # form. The literal `\\u0026` in source produces the Regexp # source `\\u0026` which matches the 6 characters `\u0026` # (not the Unicode escape for `&` — which is what # `\u0026` in source would mean). @log_filter_strict ||= %r{ https?://[^\s'"<>\\]+ # URL prefix (excludes \) (?:[?&]|\\u0026) # separator: & or \u0026 (?: X-Amz-Signature | X-Amz-Credential | X-Amz-Security-Token | X-Amz-Algorithm | X-Amz-Date | X-Amz-Expires | X-Amz-SignedHeaders | AWSAccessKeyId | Key-Pair-Id ) =[^&\s'"<>\\]+ # signature value (excludes \) (?:(?:&|\\u0026)[^\s'"<>\\]*)? # trailing params }xi.freeze end |
.parse_class ⇒ Model::TYPE_FILE
139 |
# File 'lib/parse/model/file.rb', line 139 def self.parse_class; TYPE_FILE; end |
.parse_presigned_expiry(url) ⇒ Time?
Parse the expiry time (UTC) of a presigned URL directly from its query parameters — the TTL is whatever the issuer chose, NEVER hardcoded SDK-side.
Supports:
- SigV4 (
X-Amz-Date=YYYYMMDDTHHMMSSZ+X-Amz-Expires=<seconds>): expiry = date + expires_seconds. - SigV2 / CloudFront (
Expires=<unix-seconds>): expiry = the raw timestamp.
Returns nil on malformed input — including a regex-valid
date string whose component values are out of range
(20260231T120000Z, leap-second seconds field 60, day 32,
month 13). Hydration of a corrupt row should not abort
attributes= with an upstream ArgumentError; the caller
sees the file with presigned_url_expires_at == nil and can
decide what to do.
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 |
# File 'lib/parse/model/file.rb', line 468 def parse_presigned_expiry(url) return nil unless url.is_a?(String) query = url.split("?", 2)[1] return nil unless query params = {} query.split("&").each do |pair| k, v = pair.split("=", 2) params[k] = v if k && v end if params["X-Amz-Date"] && params["X-Amz-Expires"] ts = params["X-Amz-Date"] secs = params["X-Amz-Expires"].to_i return nil unless secs > 0 # X-Amz-Date is ISO 8601 basic — YYYYMMDDTHHMMSSZ, always # UTC. Manual slice is safer than `Time.strptime` which # treats `Z` as a literal and interprets the result in # local time. m = ts.match(/\A(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z\z/) return nil unless m begin Time.utc(m[1].to_i, m[2].to_i, m[3].to_i, m[4].to_i, m[5].to_i, m[6].to_i) + secs rescue ArgumentError # Regex-valid but date-component-invalid (day 32, month # 13, seconds 60). Return nil rather than propagating up # through hydration. nil end elsif params["Expires"] unix = params["Expires"].to_i return nil if unix <= 0 Time.at(unix).utc end end |
.strip_query(url) ⇒ String?
Strip the query string (everything from the first ?) from a URL.
Used to drop short-TTL presigned-URL signature parameters before a
File.basename comparison. Implemented with String#index rather
than a sub(/\?.*\z/, "") regex: the regex form is O(n^2) on
adversarial input (a long run of ?), and these URLs are
externally influenced (S3/CloudFront presign on every read), so the
linear form removes the polynomial-regex slow path.
516 517 518 519 520 |
# File 'lib/parse/model/file.rb', line 516 def strip_query(url) return url unless url.is_a?(String) i = url.index("?") i ? url[0, i] : url end |
.url_signature_param?(url_string) ⇒ Boolean
True when the URL's query string carries any known signed-URL
parameter from SIGNATURE_QUERY_PARAMS. Used by the URL
normalization point (#normalize_and_store_url).
Uses String#include? for cheap substring detection rather
than building a Regexp on every assignment.
Case-folds the comparison so misbehaving CDNs / reverse
proxies that lowercase query-parameter names (rare but real)
do not bypass detection. AWS's canonical capitalization is
what SIGNATURE_QUERY_PARAMS is written in; the case-fold
is purely defensive.
Known limitations (documented for callers wiring custom strict-mode checks via this predicate):
- URL-encoded query separators (
?written as%3F) bypass the literal?<param>=substring match. Decode percent encoding before passing in if the URL came from a context that double-encodes. - URL fragments (
#) before a?placeholder do not get stripped here —normalize_and_store_urlhandles fragment-aware stripping during the actual URL store.
437 438 439 440 441 442 443 444 445 |
# File 'lib/parse/model/file.rb', line 437 def url_signature_param?(url_string) return false unless url_string.is_a?(String) return false unless url_string.include?("?") || url_string.include?("&") haystack = url_string.downcase SIGNATURE_QUERY_PARAMS.any? do |param| needle = param.downcase haystack.include?("?#{needle}=") || haystack.include?("&#{needle}=") end end |
Instance Method Details
#==(u) ⇒ Boolean
Returns Two files are equal if they have the same url.
738 739 740 741 |
# File 'lib/parse/model/file.rb', line 738 def ==(u) return false unless u.is_a?(self.class) @url == u.url end |
#attributes ⇒ Hash
733 734 735 |
# File 'lib/parse/model/file.rb', line 733 def attributes ATTRIBUTES end |
#attributes=(h) ⇒ Object
Allows mass assignment from a Parse JSON hash.
Routes through the single normalization point
#normalize_and_store_url, identical to #url=. Signed URLs
(the common Parse-Server-S3 case) are silently stripped and
stashed in @presigned_url. See rev 3 D2 in
s3_adapter_plan.md — asymmetric writer behavior is an explicit
anti-goal.
751 752 753 754 755 756 757 758 759 760 761 |
# File 'lib/parse/model/file.rb', line 751 def attributes=(h) raw_url = nil if h.is_a?(String) raw_url = h @name = File.basename(h) elsif h.is_a?(Hash) raw_url = h[FIELD_URL] || h[:url] @name = h[FIELD_NAME] || h[:name] || @name end normalize_and_store_url(raw_url) end |
#parse_class ⇒ Model::TYPE_FILE Also known as: __type
141 |
# File 'lib/parse/model/file.rb', line 141 def parse_class; self.class.parse_class; end |
#parse_hosted_file? ⇒ Boolean
Returns true if this file is hosted by Parse's servers.
995 996 997 998 |
# File 'lib/parse/model/file.rb', line 995 def parse_hosted_file? return false if @url.blank? ::File.basename(@url).starts_with?("tfss-") || @url.starts_with?("http://files.parsetfss.com") end |
#presigned_url_valid?(buffer: 60) ⇒ Boolean
True when #presigned_url is set and not yet expired (with an optional safety buffer so callers can refetch before the URL actually expires server-side).
799 800 801 802 803 |
# File 'lib/parse/model/file.rb', line 799 def presigned_url_valid?(buffer: 60) return false if @presigned_url.nil? return false if @presigned_url_expires_at.nil? (@presigned_url_expires_at - buffer.to_f) > Time.now.utc end |
#save(session_token: nil, use_master_key: nil) ⇒ Boolean
Save the file by uploading it to Parse and creating a file pointer.
960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 |
# File 'lib/parse/model/file.rb', line 960 def save(session_token: nil, use_master_key: nil) unless saved? || @contents.nil? || @name.nil? opts = {} opts[:session_token] = session_token unless session_token.nil? opts[:use_master_key] = use_master_key unless use_master_key.nil? response = client.create_file(@name, @contents, @mime_type, **opts) unless response.error? result = response.result # Route the create-response URL through the SAME normalization # point as `url=` / `attributes=`. Parse Server's S3FilesAdapter # can return a freshly-signed URL in the file-create response # (not only on read), and a direct `@url = result[url]` would # leave that signed URL verbatim in `@url` — and bake the # signature query string into `@name` via `File.basename` when # the response omits `name`. Normalizing here keeps the `@url` # invariant (canonical, never a short-TTL signed URL) on the # save writer too, stashes any signature in `@presigned_url`, # and honors `signed_url_policy = :raise`. # # Set `@name` to the server's authoritative name (or nil) # BEFORE normalizing so `sanitize_hydrated_url`'s `tfss-` host # check reads the response URL's own basename rather than a # stale pre-upload name. As before, `@name` is always taken # from the response — but the fallback now derives from the # CANONICAL `@url`, never the signed URL. result_name = result[FIELD_NAME] @name = result_name normalize_and_store_url(result[FIELD_URL]) @name = result_name || (@url.present? ? File.basename(@url) : nil) end end saved? end |
#saved? ⇒ Boolean
A File object is considered saved when @url and @name are
both present and @name matches the basename of @url's path
component.
The URL's query string is stripped before the basename
computation so short-TTL presigned URLs that Parse Server's
S3FilesAdapter returns on every read
(https://bucket.s3.../doc.pdf?X-Amz-Signature=...) don't
confuse File.basename into including the signature bytes in
the comparison.
716 717 718 719 720 |
# File 'lib/parse/model/file.rb', line 716 def saved? return false unless @url.present? && @name.present? path_only = Parse::File.strip_query(@url) @name == File.basename(path_only) end |
#to_s ⇒ String
Returns the url.
1016 1017 1018 |
# File 'lib/parse/model/file.rb', line 1016 def to_s @url end |
#url ⇒ String
Returns the url string for this Parse::File pointer. If the force_ssl option is set to true, it will make sure it returns a secure url.
725 726 727 728 729 730 |
# File 'lib/parse/model/file.rb', line 725 def url if @url.present? && Parse::File.force_ssl && @url.starts_with?("http://") return @url.sub("http://", "https://") end @url end |
#url=(value) ⇒ Object
Assign the file's URL.
Routes through the single normalization point #normalize_and_store_url, which is also called by #attributes= on hydration. The rule (see s3_adapter_plan.md rev 3, D1/D2) applies uniformly to every writer:
- Signed URLs (query string carries
X-Amz-Signature/X-Amz-Credential/X-Amz-Security-Token/AWSAccessKeyId/Key-Pair-Id) are silently normalized: the query string is stripped, the bare canonical URL is stored in@url, the original signed URL is stashed in@presigned_urlwith its data-driven expiry parsed from the query params themselves (X-Amz-Date + X-Amz-Expiresfor SigV4,Expiresfor legacy / CloudFront). - Trusted-host check via sanitize_hydrated_url still applies.
- The
@keycache is invalidated — URL reassignment may point at a different storage location.
No raise on signed URLs. The Wave A SignedUrlError class is
still defined for downstream apps that want stricter
enforcement (e.g. operators who can guarantee Parse Server is
NOT configured with S3FilesAdapter and want presigned URLs to
raise instead of normalize), but the built-in SDK writers do
not raise it. Asymmetric behavior between writers (raise here,
accept there) was an explicit anti-goal in rev 3 — it grows
footguns through assign_attributes / serializer round-trips.
128 129 130 |
# File 'lib/parse/model/file.rb', line 128 def url=(value) normalize_and_store_url(value) end |