# OpenProject is a project management system. # # Copyright (C) 2012-2013 the OpenProject Team # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. # # See doc/COPYRIGHT.rdoc for more details. #++ require_relative 'utils' module Migration::Utils module AttachableUtils MissingAttachment = Struct.new(:journaled_id, :journaled_type, :attachment_id, :filename, :last_version) def add_missing_attachable_journals result = missing_attachments repair_journals(result) end def repair_attachable_journal_entries(journal_type, legacy_journal_type) result = invalid_attachments(legacy_journal_type) result.map { |m| m.journaled_type = journal_type } repair_journals(result) end def remove_initial_journal_entries(journal_type, legacy_journal_type) result = invalid_attachments(legacy_journal_type) result.map { |m| m.journaled_type = journal_type } remove_initial_journals(result) end def repair_journals(result) result.each do |m| journal_ids = affected_journal_ids(m.journaled_id, m.last_version, m.journaled_type) journal_ids.each do |journal_id| insert <<-SQL INSERT INTO attachable_journals (journal_id, attachment_id, filename) VALUES (#{journal_id}, #{m.attachment_id}, '#{m.filename}') SQL end end end def remove_initial_journals(result) result.each do |m| journal_ids = affected_journal_ids(m.journaled_id, m.last_version, m.journaled_type) delete <<-SQL DELETE FROM attachable_journals WHERE journal_id IN (#{journal_ids.join(", ")}) SQL end end private def missing_attachments begin result = select_all <<-SQL SELECT * FROM ( SELECT a.container_id AS journaled_id, a.container_type AS journaled_type, a.id AS attachment_id, a.filename, MAX(aj.id) AS aj_id, MAX(j.version) AS last_version FROM attachments AS a JOIN journals AS j ON (a.container_id = j.journable_id AND a.container_type = j.journable_type) LEFT JOIN attachable_journals AS aj ON (a.id = aj.attachment_id) GROUP BY a.container_id, a.container_type, a.id, a.filename ) AS tmp WHERE aj_id IS NULL SQL rescue ActiveRecord::StatementInvalid => ex raise ex unless mysql? raise "An MySQL error occured (see details below)!"\ "\n\n"\ "If you're facing an 'Illegal mix of collations error, consider "\ "running rake task "\ "'migrations:journals:fix_attachments_collation'."\ "\n\n"\ "#{ex.message}" end result.collect { |row| MissingAttachment.new(row['journaled_id'], row['journaled_type'], row['attachment_id'], row['filename'], row['last_version']) } end COLUMNS = ['changed_data', 'version', 'journaled_id'] def invalid_attachments(legacy_journal_type) result = [] update_column_values('legacy_journals', COLUMNS, find_work_packages_with_missing_initial_attachment(legacy_journal_type, result), journal_filter(legacy_journal_type)) result.flatten end def journal_filter(legacy_journal_type) "type = '#{legacy_journal_type}' AND changed_data LIKE '%attachments%'" end def find_work_packages_with_missing_initial_attachment(legacy_journal_type, result) Proc.new do |row| missing_entries = missing_initial_attachable_journals(legacy_journal_type, row['id'], row['journaled_id'], row['version'], row['changed_data']) result << missing_entries unless missing_entries.empty? UpdateResult.new(row, false) end end def missing_initial_attachable_journals(legacy_journal_type, journal_id, journaled_id, version, changed_data) removed_attachments = parse_attachment_removals(changed_data) missing_entries = missing_initial_attachment_entries(legacy_journal_type, journaled_id, version, removed_attachments) missing_entries.map { |e| MissingAttachment.new(journaled_id, nil, e[:id], e[:filename], version.to_i - 1) } end ############################################ # Matches attachment removals of the form: # # # # attachments: # # - # # - # ############################################ ATTACHMENT_REMOVAL_REGEX = /attachments_(?\d+): \n-\s(?.+)\n-\s$/ def parse_attachment_removals(changed_data) matches = changed_data.scan(ATTACHMENT_REMOVAL_REGEX) matches.each_with_object([]) { |m, l| l << { id: m[0], filename: m[1] } } end def missing_initial_attachment_entries(legacy_journal_type, journaled_id, version, attachments) attachments.select do |a| result = select_all <<-SQL SELECT version FROM legacy_journals WHERE journaled_id = #{journaled_id} AND type = '#{legacy_journal_type}' AND version < #{version} AND changed_data LIKE '%attachments#{a[:id]}:%' AND changed_data LIKE '%- #{a[:filename]}%' ORDER BY version SQL result.empty? end end def affected_journal_ids(journaled_id, last_version, journal_type) result_set = select_all <<-SQL SELECT id FROM journals WHERE journable_id = #{journaled_id} AND journable_type = '#{journal_type}' AND version <= #{last_version} SQL result_set.collect { |r| r['id'] } end end end