Merge branch 'master' into 5374-hide-collection-checkboxes-anonymous
[arvados.git] / services / api / db / migrate / 20150303210106_fix_collection_portable_data_hash_with_hinted_manifest.rb
1 require 'has_uuid'
2 require 'kind_and_etag'
3
4 class FixCollectionPortableDataHashWithHintedManifest < ActiveRecord::Migration
5   include CurrentApiClient
6
7   class ArvadosModel < ActiveRecord::Base
8     self.abstract_class = true
9     extend HasUuid::ClassMethods
10     include CurrentApiClient
11     include KindAndEtag
12     before_create do |record|
13       record.uuid ||= record.class.generate_uuid
14       record.owner_uuid ||= system_user_uuid
15     end
16     serialize :properties, Hash
17
18     def self.to_s
19       # Clean up the name of the stub model class so we generate correct UUIDs.
20       super.sub("FixCollectionPortableDataHashWithHintedManifest::", "")
21     end
22   end
23
24   class Collection < ArvadosModel
25   end
26
27   class Log < ArvadosModel
28     def self.log_for(thing, age="old")
29       { "#{age}_etag" => thing.etag,
30         "#{age}_attributes" => thing.attributes,
31       }
32     end
33
34     def self.log_create(thing)
35       new_log("create", thing, log_for(thing, "new"))
36     end
37
38     def self.log_update(thing, start_state)
39       new_log("update", thing, start_state.merge(log_for(thing, "new")))
40     end
41
42     def self.log_destroy(thing)
43       new_log("destroy", thing, log_for(thing, "old"))
44     end
45
46     private
47
48     def self.new_log(event_type, thing, properties)
49       create!(event_type: event_type,
50               event_at: Time.now,
51               object_uuid: thing.uuid,
52               object_owner_uuid: thing.owner_uuid,
53               properties: properties)
54     end
55   end
56
57   def each_bad_collection
58     Collection.find_each do |coll|
59       next unless (coll.manifest_text =~ /\+[A-Z]/)
60       stripped_manifest = coll.manifest_text.
61         gsub(/( [0-9a-f]{32}(\+\d+)?)(\+\S+)/, '\1')
62       stripped_pdh = sprintf("%s+%i",
63                              Digest::MD5.hexdigest(stripped_manifest),
64                              stripped_manifest.bytesize)
65       yield [coll, stripped_pdh] if (coll.portable_data_hash != stripped_pdh)
66     end
67   end
68
69   def up
70     Collection.reset_column_information
71     Log.reset_column_information
72     copied_attr_names =
73       [:owner_uuid, :created_at, :modified_by_client_uuid, :manifest_text,
74        :modified_by_user_uuid, :modified_at, :updated_at, :name,
75        :description, :portable_data_hash, :replication_desired,
76        :replication_confirmed, :replication_confirmed_at, :expires_at]
77     new_expiry = Date.new(2038, 1, 31)
78
79     each_bad_collection do |coll, stripped_pdh|
80       # Create a copy of the collection including bad portable data hash,
81       # with an expiration.  This makes it possible to resolve the bad
82       # portable data hash, but the expiration can hide the Collection
83       # from more user-friendly interfaces like Workbench.
84       start_log = Log.log_for(coll)
85       attributes = Hash[copied_attr_names.map { |key| [key, coll.send(key)] }]
86       attributes[:expires_at] ||= new_expiry
87       attributes[:properties] = (coll.properties.dup rescue {})
88       attributes[:properties]["migrated_from"] ||= coll.uuid
89       coll_copy = Collection.create!(attributes)
90       Log.log_create(coll_copy)
91       coll.update_attributes(portable_data_hash: stripped_pdh)
92       Log.log_update(coll, start_log)
93     end
94   end
95
96   def down
97     Collection.reset_column_information
98     Log.reset_column_information
99     each_bad_collection do |coll, stripped_pdh|
100       if ((src_uuid = coll.properties["migrated_from"]) and
101           (src_coll = Collection.where(uuid: src_uuid).first) and
102           (src_coll.portable_data_hash == stripped_pdh))
103         start_log = Log.log_for(src_coll)
104         src_coll.portable_data_hash = coll.portable_data_hash
105         src_coll.save!
106         Log.log_update(src_coll, start_log)
107         coll.destroy or raise Exception.new("failed to destroy old collection")
108         Log.log_destroy(coll)
109       end
110     end
111   end
112 end