Class: Parse::File

Inherits:
Model
  • Object
show all
Defined in:
lib/parse/model/file.rb

Overview

Note:

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.

Examples:

file = File.open("file_path.jpg")
contents = file.read
file = Parse::File.new("myimage.jpg", contents , "image/jpeg")
file.saved? # => false
file.save

file.url # https://files.parsetfss.com/....

# or create and upload a remote file (auto-detected mime type)
file = Parse::File.create(some_url)

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}. The key field on Parse::File is 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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Model

#dirty?, find_class

Methods included from Client::Connectable

#client

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

Parameters:

  • name (String)
  • contents (Object) (defaults to: nil)
  • mime_type (String) (defaults to: nil)

    Default see default_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_hostsObject



185
186
187
# File 'lib/parse/model/file.rb', line 185

def allowed_remote_hosts
  @allowed_remote_hosts ||= []
end

.allowed_remote_portsObject



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_typeString

Returns The default mime type for created instances. Default: 'image/jpeg'.

Returns:

  • (String)

    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_sslBoolean

Returns When set to true, it will make all calls to File#url.

Returns:

  • (Boolean)

    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_sizeObject



169
170
171
# File 'lib/parse/model/file.rb', line 169

def max_remote_size
  @max_remote_size ||= DEFAULT_MAX_REMOTE_SIZE
end

.remote_timeoutObject



175
176
177
# File 'lib/parse/model/file.rb', line 175

def remote_timeout
  @remote_timeout ||= DEFAULT_REMOTE_TIMEOUT
end

.signed_url_policyObject



229
230
231
# File 'lib/parse/model/file.rb', line 229

def signed_url_policy
  @signed_url_policy ||= :strip
end

.trusted_url_hostsObject



205
206
207
# File 'lib/parse/model/file.rb', line 205

def trusted_url_hosts
  @trusted_url_hosts ||= ["files.parsetfss.com"]
end

.untrusted_url_policyObject



247
248
249
# File 'lib/parse/model/file.rb', line 247

def untrusted_url_policy
  @untrusted_url_policy ||= :warn
end

Instance Attribute Details

#contentsObject

Returns the contents of the file.

Returns:

  • (Object)

    the contents of the file.



133
134
135
# File 'lib/parse/model/file.rb', line 133

def contents
  @contents
end

#mime_typeString

Returns the mime-type of the file whe.

Returns:

  • (String)

    the mime-type of the file whe



136
137
138
# File 'lib/parse/model/file.rb', line 136

def mime_type
  @mime_type
end

#nameString

Returns the name of the file including extension (if any).

Returns:

  • (String)

    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_urlString? (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.

Returns:

  • (String, nil)

    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_atTime? (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.

Returns:

  • (Time, nil)

    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

Parameters:

Returns:

  • (String)

    File.basename(file_name)

See Also:

  • 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_namesArray<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.

Returns:



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

Parameters:

  • url (String)

    A url which will be used to create the file and automatically save it.

Returns:

  • (Parse::File)

    A newly saved file based on contents of url



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_namesArray<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.

Returns:



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_filterRegexp

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.

Examples:

Rails — scrub presigned URLs out of all log lines

config.lograge.custom_payload do |controller|
  payload = { ... }
  payload.transform_values do |v|
    v.is_a?(String) ? v.gsub(Parse::File.log_filter, "[FILTERED_PRESIGNED_URL]") : v
  end
end

Rails — filter_parameters for params with these names

Rails.application.config.filter_parameters += Parse::File.filter_parameter_names

Returns:

  • (Regexp)


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_strictRegexp

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.

Examples:

Sentry beforeSend hook — scrub both shapes

Sentry.init do |config|
  config.before_send = ->(event, _hint) {
    json = JSON.dump(event.to_hash)
    scrubbed = json.gsub(Parse::File.log_filter_strict, "[FILTERED_PRESIGNED_URL]")
    JSON.parse(scrubbed)
  }
end

Returns:

  • (Regexp)


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_classModel::TYPE_FILE

Returns:



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.

Parameters:

Returns:

  • (Time, nil)

    expiry in UTC, or nil if the URL doesn't carry parseable presigned-URL expiry data.



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.

Parameters:

Returns:

  • (String, nil)

    the URL up to (but excluding) the first ?, or the input unchanged when there is no query string. nil and non-strings pass through untouched.



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_url handles fragment-aware stripping during the actual URL store.

Parameters:

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)

    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

#attributesHash

Returns:



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_classModel::TYPE_FILE Also known as: __type

Returns:



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.

Returns:

  • (Boolean)

    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).

Examples:

Render a presigned URL in a Rails view, refetching when near expiry

if file.presigned_url_valid?
  # render directly — buffer absorbs network RTT + retries
else
  post.reload
  # render post.attachment.presigned_url
end

Parameters:

  • buffer (Integer, Float) (defaults to: 60)

    seconds before presigned_url_expires_at to start treating as expired. Default 60 seconds — a margin that absorbs network RTT, client clock skew, and one retry. Tighten via buffer: 30 in latency-sensitive paths; loosen via buffer: 120 for apps that proxy URLs through additional hops before render.

Returns:

  • (Boolean)


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.

Parameters:

  • session_token (String, nil) (defaults to: nil)

    thread an authenticated user's session token through the upload. Required when the SDK is running in client mode against a Parse Server with fileUpload.enableForAuthenticatedUser on (the typical safe configuration). When nil, the upload uses whatever auth the default client carries — which for client-mode builds is anonymous.

  • use_master_key (Boolean, nil) (defaults to: nil)

    explicitly opt in or out of master-key auth for this upload. Defaults to the client's configured behavior. Pass false in client-mode code to assert that no master key is smuggled into the upload.

Returns:

  • (Boolean)

    true if successfully uploaded and saved.



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.

Returns:

  • (Boolean)

    true if this file has already been saved.



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_sString

Returns the url.

Returns:

See Also:



1016
1017
1018
# File 'lib/parse/model/file.rb', line 1016

def to_s
  @url
end

#urlString

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.

Returns:

  • (String)

    the url string for the file.



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_url with its data-driven expiry parsed from the query params themselves (X-Amz-Date + X-Amz-Expires for SigV4, Expires for legacy / CloudFront).
  • Trusted-host check via sanitize_hydrated_url still applies.
  • The @key cache 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.

Parameters:

  • value (String, nil)

    the URL to assign.



128
129
130
# File 'lib/parse/model/file.rb', line 128

def url=(value)
  normalize_and_store_url(value)
end