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_portsfor 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::SYSTEM_CLASS_MAP, 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 when their #content_signature matches.
- #attributes ⇒ Hash
-
#attributes=(h) ⇒ Object
Allows mass assignment from a Parse JSON hash.
- #content_signature ⇒ String?
-
#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
705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 |
# File 'lib/parse/model/file.rb', line 705 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
187 188 189 |
# File 'lib/parse/model/file.rb', line 187 def allowed_remote_hosts @allowed_remote_hosts ||= [] end |
.allowed_remote_ports ⇒ Object
259 260 261 |
# File 'lib/parse/model/file.rb', line 259 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'.
156 157 158 |
# File 'lib/parse/model/file.rb', line 156 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.
161 162 163 |
# File 'lib/parse/model/file.rb', line 161 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
176 177 178 |
# File 'lib/parse/model/file.rb', line 176 def remote_timeout @remote_timeout ||= DEFAULT_REMOTE_TIMEOUT end |
.signed_url_policy ⇒ Object
233 234 235 |
# File 'lib/parse/model/file.rb', line 233 def signed_url_policy @signed_url_policy ||= :strip end |
.trusted_url_hosts ⇒ Object
208 209 210 |
# File 'lib/parse/model/file.rb', line 208 def trusted_url_hosts @trusted_url_hosts ||= ["files.parsetfss.com"] end |
.untrusted_url_policy ⇒ Object
252 253 254 |
# File 'lib/parse/model/file.rb', line 252 def untrusted_url_policy @untrusted_url_policy ||= :warn end |
Instance Attribute Details
#contents ⇒ Object
Returns the contents of the file.
132 133 134 |
# File 'lib/parse/model/file.rb', line 132 def contents @contents end |
#mime_type ⇒ String
Returns the mime-type of the file whe.
135 136 137 |
# File 'lib/parse/model/file.rb', line 135 def mime_type @mime_type end |
#name ⇒ String
Returns the name of the file including extension (if any).
96 97 98 |
# File 'lib/parse/model/file.rb', line 96 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.
849 850 851 |
# File 'lib/parse/model/file.rb', line 849 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.
857 858 859 |
# File 'lib/parse/model/file.rb', line 857 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
1020 1021 1022 1023 1024 1025 1026 |
# File 'lib/parse/model/file.rb', line 1020 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.
410 411 412 413 414 415 416 |
# File 'lib/parse/model/file.rb', line 410 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
743 744 745 746 747 748 |
# File 'lib/parse/model/file.rb', line 743 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.
382 383 384 385 386 387 388 |
# File 'lib/parse/model/file.rb', line 382 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.
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 |
# File 'lib/parse/model/file.rb', line 307 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.
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 |
# File 'lib/parse/model/file.rb', line 343 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
138 |
# File 'lib/parse/model/file.rb', line 138 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.
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 502 503 504 505 506 507 |
# File 'lib/parse/model/file.rb', line 474 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.
522 523 524 525 526 |
# File 'lib/parse/model/file.rb', line 522 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.
443 444 445 446 447 448 449 450 451 |
# File 'lib/parse/model/file.rb', line 443 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 when their #content_signature
matches. Both sides go through the same reader, so the comparison is
symmetric and force_ssl-consistent: the previous @url == u.url form
compared one side's raw stored URL against the other's normalized
reader, so two files at the same location read as unequal whenever
force_ssl coerced one side from http:// to https://
(and a == b disagreed with b == a). Because the default signature is
the bare canonical URL (signed-URL query parameters stripped into
@presigned_url), a freshly re-signed URL for the same object is equal
while a different underlying location is not. See #content_signature
for the content-hash override seam.
817 818 819 820 |
# File 'lib/parse/model/file.rb', line 817 def ==(u) return false unless u.is_a?(self.class) content_signature == u.content_signature end |
#attributes ⇒ Hash
779 780 781 |
# File 'lib/parse/model/file.rb', line 779 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.
830 831 832 833 834 835 836 837 838 839 840 |
# File 'lib/parse/model/file.rb', line 830 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 |
#content_signature ⇒ String?
The value used to decide whether two Parse::Files refer to the same
underlying file -- for equality (#==) and, through it, for dirty
tracking (the property setter compares files with == to decide whether
a :file field changed).
Today this is the bare canonical #url: signed-URL query parameters are
stripped into @presigned_url and force_ssl coercion is applied, so two
files at the same storage location compare equal regardless of how the URL
was signed or whether it was http/https. The URL is the best identity
signal currently available -- Parse Server's S3 files adapter does not
surface a content digest (ETag / MD5 / sha256) through Parse::File.
FUTURE DIRECTION: when a files adapter can expose a content hash, this is
the single seam to override so equality keys off file content instead of
URL -- e.g. a custom Parse::File subclass or adapter shim returning the
S3 ETag / sha256 here. Overriding this one method updates #== (and a
future #eql?/#hash pair, if added) without touching dirty tracking.
No content-hash source exists yet, so the URL is authoritative for now.
802 803 804 |
# File 'lib/parse/model/file.rb', line 802 def content_signature url end |
#parse_class ⇒ Model::TYPE_FILE Also known as: __type
140 |
# File 'lib/parse/model/file.rb', line 140 def parse_class; self.class.parse_class; end |
#parse_hosted_file? ⇒ Boolean
Returns true if this file is hosted by Parse's servers.
1075 1076 1077 1078 |
# File 'lib/parse/model/file.rb', line 1075 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).
878 879 880 881 882 |
# File 'lib/parse/model/file.rb', line 878 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.
1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 |
# File 'lib/parse/model/file.rb', line 1040 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.
762 763 764 765 766 |
# File 'lib/parse/model/file.rb', line 762 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.
1096 1097 1098 |
# File 'lib/parse/model/file.rb', line 1096 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.
771 772 773 774 775 776 |
# File 'lib/parse/model/file.rb', line 771 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.
127 128 129 |
# File 'lib/parse/model/file.rb', line 127 def url=(value) normalize_and_store_url(value) end |