summaryrefslogtreecommitdiff
path: root/app/services/design_management/save_designs_service.rb
blob: ea5675c6ddd94a2f454151d1bbb4663c59264a16 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# frozen_string_literal: true

module DesignManagement
  class SaveDesignsService < DesignService
    include RunsDesignActions
    include OnSuccessCallbacks

    MAX_FILES = 10

    def initialize(project, user, params = {})
      super

      @files = params.fetch(:files)
    end

    def execute
      return error("Not allowed!") unless can_create_designs?
      return error("Only #{MAX_FILES} files are allowed simultaneously") if files.size > MAX_FILES
      return error("Duplicate filenames are not allowed!") if files.map(&:original_filename).uniq.length != files.length
      return error("Design copy is in progress") if design_collection.copy_in_progress?

      uploaded_designs, version = upload_designs!
      skipped_designs = designs - uploaded_designs

      create_events
      design_collection.reset_copy!

      success({ designs: uploaded_designs, version: version, skipped_designs: skipped_designs })
    rescue ::ActiveRecord::RecordInvalid => e
      error(e.message)
    end

    private

    attr_reader :files

    def upload_designs!
      ::DesignManagement::Version.with_lock(project.id, repository) do
        actions = build_actions

        [
          actions.map(&:design),
          actions.presence && run_actions(actions)
        ]
      end
    end

    # Returns `Design` instances that correspond with `files`.
    # New `Design`s will be created where a file name does not match
    # an existing `Design`
    def designs
      @designs ||= files.map do |file|
        collection.find_or_create_design!(filename: file.original_filename)
      end
    end

    def build_actions
      @actions ||= files.zip(designs).flat_map do |(file, design)|
        Array.wrap(build_design_action(file, design))
      end
    end

    def build_design_action(file, design)
      content = file_content(file, design.full_path)
      return if design_unchanged?(design, content)

      action = new_file?(design) ? :create : :update
      on_success do
        track_usage_metrics(action)
      end

      DesignManagement::DesignAction.new(design, action, content)
    end

    # Returns true if the design file is the same as its latest version
    def design_unchanged?(design, content)
      content == existing_blobs[design]&.data
    end

    def create_events
      by_action = @actions.group_by(&:action).transform_values { |grp| grp.map(&:design) }

      event_create_service.save_designs(current_user, **by_action)
    end

    def event_create_service
      @event_create_service ||= EventCreateService.new
    end

    def commit_message
      <<~MSG
      Updated #{files.size} #{'designs'.pluralize(files.size)}

      #{formatted_file_list}
      MSG
    end

    def formatted_file_list
      filenames.map { |name| "- #{name}" }.join("\n")
    end

    def filenames
      @filenames ||= files.map(&:original_filename)
    end

    def can_create_designs?
      Ability.allowed?(current_user, :create_design, issue)
    end

    def new_file?(design)
      !existing_blobs[design]
    end

    def file_content(file, full_path)
      transformer = ::Lfs::FileTransformer.new(project, repository, target_branch)
      transformer.new_file(full_path, file.to_io, detect_content_type: Feature.enabled?(:design_management_allow_dangerous_images, project)).content
    end

    # Returns the latest blobs for the designs as a Hash of `{ Design => Blob }`
    def existing_blobs
      @existing_blobs ||= begin
        items = designs.map { |d| [target_branch, d.full_path] }

        repository.blobs_at(items).each_with_object({}) do |blob, h|
          design = designs.find { |d| d.full_path == blob.path }

          h[design] = blob
        end
      end
    end

    def track_usage_metrics(action)
      if action == :update
        ::Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_designs_modified_action(author: current_user,
                                                                                                    project: project)
      else
        ::Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_designs_added_action(author: current_user,
                                                                                                 project: project)
      end

      ::Gitlab::UsageDataCounters::DesignsCounter.count(action)
    end
  end
end

DesignManagement::SaveDesignsService.prepend_mod_with('DesignManagement::SaveDesignsService')