serialize :properties, Hash
+ before_validation :default_empty_manifest
before_validation :check_encoding
+ before_validation :check_manifest_validity
before_validation :check_signatures
before_validation :strip_signatures_and_update_replication_confirmed
validate :ensure_pdh_matches_manifest_text
before_save :set_file_names
+ before_save :expires_at_not_in_past
# Query only undeleted collections by default.
- default_scope where("expires_at IS NULL or expires_at > CURRENT_TIMESTAMP")
+ default_scope where("expires_at IS NULL or expires_at > statement_timestamp()")
api_accessible :user, extend: :common do |t|
t.add :name
t.add :replication_desired
t.add :replication_confirmed
t.add :replication_confirmed_at
+ t.add :expires_at
end
def self.attributes_required_columns
# expose signed_manifest_text as manifest_text in the
# API response, and never let clients select the
# manifest_text column.
- 'manifest_text' => ['manifest_text'],
+ #
+ # We need expires_at to determine the correct
+ # timestamp in signed_manifest_text.
+ 'manifest_text' => ['manifest_text', 'expires_at'],
)
end
now: db_current_time.to_i,
}
self.manifest_text.each_line do |entry|
- entry.split[1..-1].each do |tok|
- if tok =~ FILE_TOKEN
+ entry.split.each do |tok|
+ if tok == '.' or tok.starts_with? './'
+ # Stream name token.
+ elsif tok =~ FILE_TOKEN
# This is a filename token, not a blob locator. Note that we
# keep checking tokens after this, even though manifest
# format dictates that all subsequent tokens will also be
true
elsif portable_data_hash.nil? or not portable_data_hash_changed?
self.portable_data_hash = computed_pdh
- elsif portable_data_hash !~ /^[0-9a-f]{32}(\+\d+)?$/
- errors.add(:portable_data_hash, "is not a valid hash or hash+size")
+ elsif portable_data_hash !~ Keep::Locator::LOCATOR_REGEXP
+ errors.add(:portable_data_hash, "is not a valid locator")
false
elsif portable_data_hash[0..31] != computed_pdh[0..31]
errors.add(:portable_data_hash,
names[0,2**12]
end
+ def default_empty_manifest
+ self.manifest_text ||= ''
+ end
+
def check_encoding
if manifest_text.encoding.name == 'UTF-8' and manifest_text.valid_encoding?
true
end
end
+ def check_manifest_validity
+ begin
+ Keep::Manifest.validate! manifest_text
+ true
+ rescue ArgumentError => e
+ errors.add :manifest_text, e.message
+ false
+ end
+ end
+
def signed_manifest_text
if has_attribute? :manifest_text
token = current_api_client_authorization.andand.api_token
- @signed_manifest_text = self.class.sign_manifest manifest_text, token
+ exp = [db_current_time.to_i + Rails.configuration.blob_signature_ttl,
+ expires_at].compact.map(&:to_i).min
+ @signed_manifest_text = self.class.sign_manifest manifest_text, token, exp
end
end
- def self.sign_manifest manifest, token
+ def self.sign_manifest manifest, token, exp=nil
+ if exp.nil?
+ exp = db_current_time.to_i + Rails.configuration.blob_signature_ttl
+ end
signing_opts = {
api_token: token,
- expire: db_current_time.to_i + Rails.configuration.blob_signature_ttl,
+ expire: exp,
}
m = munge_manifest_locators(manifest) do |match|
Blob.sign_locator(match[0], signing_opts)
line.rstrip!
new_words = []
line.split(' ').each do |word|
- if match = Keep::Locator::LOCATOR_REGEXP.match(word)
+ if new_words.empty?
+ new_words << word
+ elsif match = Keep::Locator::LOCATOR_REGEXP.match(word)
new_words << yield(match)
else
new_words << word
manifest.each_line do |line|
# line will have a trailing newline, but the last token is never
# a locator, so it's harmless here.
- line.each_line(' ') do |word|
+ line.split(' ').each do |word|
if match = Keep::Locator::LOCATOR_REGEXP.match(word)
yield(match)
end
super - ["manifest_text"]
end
+ def logged_attributes
+ attrs = attributes.dup
+ attrs.delete('manifest_text')
+ attrs
+ end
+
protected
def portable_manifest_text
self.class.munge_manifest_locators(manifest_text) do |match|
end
super
end
+
+ # If expires_at is being changed to a time in the past, change it to
+ # now. This allows clients to say "expires {client-current-time}"
+ # without failing due to clock skew, while avoiding odd log entries
+ # like "expiry date changed to {1 year ago}".
+ def expires_at_not_in_past
+ if expires_at_changed? and expires_at
+ self.expires_at = [db_current_time, expires_at].max
+ end
+ end
end