opf/openproject

View on GitHub
lib/tasks/shared/legacy_attachment.rb

Summary

Maintainability
A
1 hr
Test Coverage
module Tasks
  module Shared
    ##
    # In the olden times (pre 4.1) files were all saved under the configured storage path,
    # most likely `files`, with a timestamp attached to their name to avoid conflicts.
    #
    # e.g. `files/140917231758_openshift-setup.txt`
    #
    # The new approach is using CarrierWave for attachments. There the files will be stored
    # some place else. Where is not material for this migration.
    # It could be another directory or S3.
    #
    # The migration does the following:
    #
    # * rename the legacy files - it strips off the timestamps
    # * assign the renamed file to the CarrierWave uploader - this moves the file to
    #   the new location
    #
    # The rollback does the opposite using the remaining legacy column `disk_filename`
    # of Attachment. Meaning it adds the timestamp again and puts the files back into
    # the configured attachment storage path.
    #
    # This assumes that the attachment storage path is the same as before the migration
    # to CarrierWave and also that the legacy columns `filename` and `disk_filename`
    # are still present.
    module LegacyAttachment
      module_function

      def migrate_attachment(attachment)
        file_name = attachment.disk_filename.presence

        if file_name
          file = legacy_file_name file_name
          new_file = strip_timestamp_from_filename(file)
        end

        if file_name && File.readable?(file)
          FileUtils.move file, new_file unless file == new_file
          attachment.file = File.open(new_file)
          attachment.filename = ""
          attachment.save!

          FileUtils.rm_f new_file

          File.readable? attachment.file.path
        else
          path = attachment.file.path
          if path && File.readable?(path)
            true # file has been migrated already
          else
            puts "Found corrupt attachment (#{attachment.id}) during migration: \
                  '#{file}' does not exist".squish
            false
          end
        end
      end

      def rollback_attachment(attachment)
        return unless attachment.file.path

        old_file = rolled_back_file_name attachment

        unless File.readable? old_file
          file = attachment.diskfile.path

          FileUtils.move file, old_file
          attachment.update_column :file, nil
          attachment.update_column :filename, Pathname(file).basename.to_s

          # keep original disk filename if it was preserved
          if attachment.disk_filename.blank?
            attachment.update_column :disk_filename, Pathname(old_file).basename.to_s
          end

          FileUtils.rmdir Pathname(file).dirname
        end
      end

      ##
      # Returns rolled back file name for an attachment.
      # If an attachment was created after the migration to CarrierWave it doesn't have an original
      # legacy name. Instead one will be generated. Not with a timestamp, however, but with a
      # random hex string as a prefix.
      #
      # This way new attachments won't be lost when rolling back to an old version of OpenProject.
      def rolled_back_file_name(attachment)
        if attachment.disk_filename.blank?
          uuid = SecureRandom.hex 4
          name = Pathname(attachment.diskfile.path).basename
          legacy_file_name "#{uuid}_#{name}"
        else
          legacy_file_name attachment.disk_filename
        end
      end

      def legacy_file_name(file_name)
        Pathname(OpenProject::Configuration.attachments_storage_path).join file_name
      end

      ##
      # This method strips the leading timestamp from a given file name and returns the plain,
      # original file name.
      def strip_timestamp_from_filename(file)
        Pathname(file).dirname + Pathname(file).basename.to_s.gsub(/^[^_]+_/, "")
      end
    end
  end
end