summaryrefslogtreecommitdiff
path: root/app/services/packages/debian/generate_distribution_service.rb
blob: 74b07e05aa68cefd8fd4d07e234d5bbe7b319065 (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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# frozen_string_literal: true

module Packages
  module Debian
    class GenerateDistributionService
      include Gitlab::Utils::StrongMemoize
      include ExclusiveLeaseGuard

      ONE_HOUR = 1.hour.freeze

      # used by ExclusiveLeaseGuard
      DEFAULT_LEASE_TIMEOUT = 1.hour.to_i.freeze

      # From https://salsa.debian.org/ftp-team/dak/-/blob/991aaa27a7f7aa773bb9c0cf2d516e383d9cffa0/setup/core-init.d/080_metadatakeys#L9
      METADATA_KEYS = %w(
        Package
        Source
        Binary
        Version
        Essential
        Installed-Size
        Maintainer
        Uploaders
        Original-Maintainer
        Build-Depends
        Build-Depends-Indep
        Build-Conflicts
        Build-Conflicts-Indep
        Architecture
        Standards-Version
        Format
        Files
        Dm-Upload-Allowed
        Vcs-Browse
        Vcs-Hg
        Vcs-Darcs
        Vcs-Svn
        Vcs-Git
        Vcs-Browser
        Vcs-Arch
        Vcs-Bzr
        Vcs-Mtn
        Vcs-Cvs
        Checksums-Sha256
        Checksums-Sha1
        Replaces
        Provides
        Depends
        Pre-Depends
        Recommends
        Suggests
        Enhances
        Conflicts
        Breaks
        Description
        Origin
        Bugs
        Multi-Arch
        Homepage
        Tag
        Package-Type
        Installer-Menu-Item
      ).freeze

      def initialize(distribution)
        @distribution = distribution
        @oldest_kept_generated_at = nil
        @md5sum = []
        @sha256 = []
      end

      def execute
        try_obtain_lease do
          @distribution.transaction do
            # We consider `apt-get update` can take at most one hour
            # We keep all generations younger than one hour
            # and the previous generation
            @oldest_kept_generated_at = @distribution.component_files.updated_before(release_date - ONE_HOUR).maximum(:updated_at)
            generate_component_files
            generate_release
            destroy_old_component_files
          end
        end
      end

      private

      def generate_component_files
        @distribution.components.ordered_by_name.each do |component|
          @distribution.architectures.ordered_by_name.each do |architecture|
            generate_component_file(component, :packages, architecture, :deb)
            generate_component_file(component, :di_packages, architecture, :udeb)
          end
          generate_component_file(component, :source, nil, :dsc)
        end
      end

      def generate_component_file(component, component_file_type, architecture, package_file_type)
        paragraphs = @distribution.package_files
                                  .preload_package
                                  .preload_debian_file_metadata
                                  .with_debian_component_name(component.name)
                                  .with_debian_architecture_name(architecture&.name)
                                  .with_debian_file_type(package_file_type)
                                  .find_each
                                  .map(&method(:package_stanza_from_fields))
        reuse_or_create_component_file(component, component_file_type, architecture, paragraphs.join("\n"))
      end

      def package_stanza_from_fields(package_file)
        [
          METADATA_KEYS.map do |metadata_key|
            metadata_name = metadata_key
            metadata_value = package_file.debian_fields[metadata_key]

            if package_file.debian_dsc?
              metadata_name = 'Package' if metadata_key == 'Source'
              checksum = case metadata_key
                         when 'Files' then package_file.file_md5
                         when 'Checksums-Sha256' then package_file.file_sha256
                         when 'Checksums-Sha1' then package_file.file_sha1
                         end

              if checksum
                metadata_value = "\n#{checksum} #{package_file.size} #{package_file.file_name}#{metadata_value}"
              end
            end

            rfc822_field(metadata_name, metadata_value)
          end,
          rfc822_field('Section', package_file.debian_fields['Section'] || 'misc'),
          rfc822_field('Priority', package_file.debian_fields['Priority'] || 'extra'),
          package_file_extra_fields(package_file)
        ].flatten.compact.join('')
      end

      def package_file_extra_fields(package_file)
        if package_file.debian_dsc?
          [
            rfc822_field('Directory', package_dirname(package_file))
          ]
        else
          [
            rfc822_field('Filename', "#{package_dirname(package_file)}/#{package_file.file_name}"),
            rfc822_field('Size', package_file.size),
            rfc822_field('MD5sum', package_file.file_md5),
            rfc822_field('SHA256', package_file.file_sha256)
          ]
        end
      end

      def package_dirname(package_file)
        letter = package_file.package.name.start_with?('lib') ? package_file.package.name[0..3] : package_file.package.name[0]
        "#{pool_prefix(package_file)}/#{letter}/#{package_file.package.name}/#{package_file.package.version}"
      end

      def pool_prefix(package_file)
        case @distribution
        when ::Packages::Debian::GroupDistribution
          "pool/#{@distribution.codename}/#{package_file.package.project_id}"
        else
          "pool/#{@distribution.codename}"
        end
      end

      def reuse_or_create_component_file(component, component_file_type, architecture, content)
        file_md5 = Digest::MD5.hexdigest(content)
        file_sha256 = Digest::SHA256.hexdigest(content)
        component_file = component.files
                                  .with_file_type(component_file_type)
                                  .with_architecture(architecture)
                                  .with_compression_type(nil)
                                  .with_file_sha256(file_sha256)
                                  .last

        if component_file
          component_file.touch(time: release_date)
        else
          component_file = component.files.create!(
            updated_at: release_date,
            file_type: component_file_type,
            architecture: architecture,
            compression_type: nil,
            file: CarrierWaveStringFile.new(content),
            file_md5: file_md5,
            file_sha256: file_sha256
          )
        end

        @md5sum.append(" #{file_md5} #{component_file.size.to_s.rjust(8)} #{component_file.relative_path}")
        @sha256.append(" #{file_sha256} #{component_file.size.to_s.rjust(8)} #{component_file.relative_path}")
      end

      def generate_release
        @distribution.key || @distribution.create_key(GenerateDistributionKeyService.new.execute)
        @distribution.file = CarrierWaveStringFile.new(release_content)
        @distribution.file_signature = SignDistributionService.new(@distribution, release_content, detach: true).execute
        @distribution.signed_file = CarrierWaveStringFile.new(
          SignDistributionService.new(@distribution, release_content).execute
        )
        @distribution.updated_at = release_date
        @distribution.save!
      end

      def release_content
        strong_memoize(:release_content) do
          release_header + release_sums
        end
      end

      def release_header
        [
          %w[origin label suite version codename].map do |attribute|
            rfc822_field(attribute.capitalize, @distribution.attributes[attribute])
          end,
          rfc822_field('Date', release_date.to_formatted_s(:rfc822)),
          valid_until_field,
          rfc822_field('NotAutomatic', !@distribution.automatic, !@distribution.automatic),
          rfc822_field('ButAutomaticUpgrades', @distribution.automatic_upgrades, !@distribution.automatic && @distribution.automatic_upgrades),
          rfc822_field('Architectures', @distribution.architectures.map { |architecture| architecture.name }.sort.join(' ')),
          rfc822_field('Components', @distribution.components.map { |component| component.name }.sort.join(' ')),
          rfc822_field('Description', @distribution.description)
        ].flatten.compact.join('')
      end

      def release_date
        strong_memoize(:release_date) do
          Time.now.utc
        end
      end

      def release_sums
        ["MD5Sum:", @md5sum, "SHA256:", @sha256].flatten.compact.join("\n") + "\n"
      end

      def rfc822_field(name, value, condition = true)
        return unless condition
        return if value.blank?

        value = " #{value}" unless value[0] == "\n"
        "#{name}:#{value.to_s.gsub("\n\n", "\n.\n").gsub("\n", "\n ")}\n"
      end

      def valid_until_field
        return unless @distribution.valid_time_duration_seconds

        rfc822_field('Valid-Until', release_date.since(@distribution.valid_time_duration_seconds).to_formatted_s(:rfc822))
      end

      def destroy_old_component_files
        return if @oldest_kept_generated_at.nil?

        @distribution.component_files.updated_before(@oldest_kept_generated_at).destroy_all # rubocop:disable Cop/DestroyAll
      end

      # used by ExclusiveLeaseGuard
      def lease_key
        "packages:debian:generate_distribution_service:distribution:#{@distribution.id}"
      end

      # used by ExclusiveLeaseGuard
      def lease_timeout
        DEFAULT_LEASE_TIMEOUT
      end
    end
  end
end