Merge branch '12626-merge-accounts'
[arvados.git] / services / api / app / models / collection.rb
index 33f6bc2524273289d3c4240b1786fc746705af6c..4772768c8fe086f1e3bc2a25ca7a134cef8d436c 100644 (file)
@@ -1,5 +1,10 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
 require 'arvados/keep'
-require 'sweep_trashed_collections'
+require 'sweep_trashed_objects'
+require 'trashable'
 
 class Collection < ArvadosModel
   extend CurrentApiClient
@@ -7,25 +12,23 @@ class Collection < ArvadosModel
   include HasUuid
   include KindAndEtag
   include CommonApiTemplate
+  include Trashable
 
   serialize :properties, Hash
+  serialize :storage_classes_desired, Array
+  serialize :storage_classes_confirmed, Array
 
-  before_validation :set_validation_timestamp
   before_validation :default_empty_manifest
+  before_validation :default_storage_classes, on: :create
   before_validation :check_encoding
   before_validation :check_manifest_validity
   before_validation :check_signatures
   before_validation :strip_signatures_and_update_replication_confirmed
-  before_validation :ensure_trash_at_not_in_past
-  before_validation :sync_trash_state
-  before_validation :default_trash_interval
   validate :ensure_pdh_matches_manifest_text
-  validate :validate_trash_and_delete_timing
+  validate :ensure_storage_classes_desired_is_not_empty
+  validate :ensure_storage_classes_contain_non_empty_strings
   before_save :set_file_names
 
-  # Query only untrashed collections by default.
-  default_scope { where("is_trashed = false") }
-
   api_accessible :user, extend: :common do |t|
     t.add :name
     t.add :description
@@ -36,6 +39,9 @@ class Collection < ArvadosModel
     t.add :replication_desired
     t.add :replication_confirmed
     t.add :replication_confirmed_at
+    t.add :storage_classes_desired
+    t.add :storage_classes_confirmed
+    t.add :storage_classes_confirmed_at
     t.add :delete_at
     t.add :trash_at
     t.add :is_trashed
@@ -65,6 +71,10 @@ class Collection < ArvadosModel
     super + ["updated_at", "file_names"]
   end
 
+  def self.limit_index_columns_read
+    ["manifest_text"]
+  end
+
   FILE_TOKEN = /^[[:digit:]]+:[[:digit:]]+:/
   def check_signatures
     return false if self.manifest_text.nil?
@@ -434,15 +444,26 @@ class Collection < ArvadosModel
   end
 
   def self.full_text_searchable_columns
-    super - ["manifest_text"]
+    super - ["manifest_text", "storage_classes_desired", "storage_classes_confirmed"]
   end
 
   def self.where *args
-    SweepTrashedCollections.sweep_if_stale
+    SweepTrashedObjects.sweep_if_stale
     super
   end
 
   protected
+
+  # Although the defaults for these columns is already set up on the schema,
+  # collection creation from an API client seems to ignore them, making the
+  # validation on empty desired storage classes return an error.
+  def default_storage_classes
+    if self.storage_classes_desired.nil? || self.storage_classes_desired.empty?
+      self.storage_classes_desired = ["default"]
+    end
+    self.storage_classes_confirmed ||= []
+  end
+
   def portable_manifest_text
     self.class.munge_manifest_locators(manifest_text) do |match|
       if match[2] # size
@@ -470,89 +491,30 @@ class Collection < ArvadosModel
   end
 
   def ensure_permission_to_save
-    if (not current_user.andand.is_admin and
-        (replication_confirmed_at_changed? or replication_confirmed_changed?) and
-        not (replication_confirmed_at.nil? and replication_confirmed.nil?))
-      raise ArvadosModel::PermissionDeniedError.new("replication_confirmed and replication_confirmed_at attributes cannot be changed, except by setting both to nil")
+    if (not current_user.andand.is_admin)
+      if (replication_confirmed_at_changed? or replication_confirmed_changed?) and
+        not (replication_confirmed_at.nil? and replication_confirmed.nil?)
+        raise ArvadosModel::PermissionDeniedError.new("replication_confirmed and replication_confirmed_at attributes cannot be changed, except by setting both to nil")
+      end
+      if (storage_classes_confirmed_changed? or storage_classes_confirmed_at_changed?) and
+        not (storage_classes_confirmed == [] and storage_classes_confirmed_at.nil?)
+        raise ArvadosModel::PermissionDeniedError.new("storage_classes_confirmed and storage_classes_confirmed_at attributes cannot be changed, except by setting them to [] and nil respectively")
+      end
     end
     super
   end
 
-  # Use a single timestamp for all validations, even though each
-  # validation runs at a different time.
-  def set_validation_timestamp
-    @validation_timestamp = db_current_time
-  end
-
-  # If trash_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 ensure_trash_at_not_in_past
-    if trash_at_changed? && trash_at
-      self.trash_at = [@validation_timestamp, trash_at].max
+  def ensure_storage_classes_desired_is_not_empty
+    if self.storage_classes_desired.empty?
+      raise ArvadosModel::InvalidStateTransitionError.new("storage_classes_desired shouldn't be empty")
     end
   end
 
-  # Caller can move into/out of trash by setting/clearing is_trashed
-  # -- however, if the caller also changes trash_at, then any changes
-  # to is_trashed are ignored.
-  def sync_trash_state
-    if is_trashed_changed? && !trash_at_changed?
-      if is_trashed
-        self.trash_at = @validation_timestamp
-      else
-        self.trash_at = nil
-        self.delete_at = nil
-      end
-    end
-    self.is_trashed = trash_at && trash_at <= @validation_timestamp || false
-    true
-  end
-
-  def default_trash_interval
-    if trash_at_changed? && !delete_at_changed?
-      # If trash_at is updated without touching delete_at,
-      # automatically update delete_at to a sensible value.
-      if trash_at.nil?
-        self.delete_at = nil
-      else
-        self.delete_at = trash_at + Rails.configuration.default_trash_lifetime.seconds
-      end
-    elsif !trash_at || !delete_at || trash_at > delete_at
-      # Not trash, or bogus arguments? Just validate in
-      # validate_trash_and_delete_timing.
-    elsif delete_at_changed? && delete_at >= trash_at
-      # Fix delete_at if needed, so it's not earlier than the expiry
-      # time on any permission tokens that might have been given out.
-
-      # In any case there are no signatures expiring after now+TTL.
-      # Also, if the existing trash_at time has already passed, we
-      # know we haven't given out any signatures since then.
-      earliest_delete = [
-        @validation_timestamp,
-        trash_at_was,
-      ].compact.min + Rails.configuration.blob_signature_ttl.seconds
-
-      # The previous value of delete_at is also an upper bound on the
-      # longest-lived permission token. For example, if TTL=14,
-      # trash_at_was=now-7, delete_at_was=now+7, then it is safe to
-      # set trash_at=now+6, delete_at=now+8.
-      earliest_delete = [earliest_delete, delete_at_was].compact.min
-
-      # If delete_at is too soon, use the earliest possible time.
-      if delete_at < earliest_delete
-        self.delete_at = earliest_delete
+  def ensure_storage_classes_contain_non_empty_strings
+    (self.storage_classes_desired + self.storage_classes_confirmed).each do |c|
+      if !c.is_a?(String) || c == ''
+        raise ArvadosModel::InvalidStateTransitionError.new("storage classes should only be non-empty strings")
       end
     end
   end
-
-  def validate_trash_and_delete_timing
-    if trash_at.nil? != delete_at.nil?
-      errors.add :delete_at, "must be set if trash_at is set, and must be nil otherwise"
-    elsif delete_at && delete_at < trash_at
-      errors.add :delete_at, "must not be earlier than trash_at"
-    end
-    true
-  end
 end