appydave-tools 0.69.0 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +12 -0
- data/CLAUDE.md +70 -0
- data/bin/dam +18 -6
- data/docs/README.md +1 -0
- data/docs/dam/batch-s3-listing-requirements.md +780 -0
- data/docs/guides/tools/video-file-namer.md +400 -0
- data/lib/appydave/tools/dam/project_listing.rb +218 -110
- data/lib/appydave/tools/dam/s3_operations.rb +110 -59
- data/lib/appydave/tools/version.rb +1 -1
- data/package.json +1 -1
- metadata +4 -2
|
@@ -11,7 +11,7 @@ module Appydave
|
|
|
11
11
|
class ProjectListing
|
|
12
12
|
# List all brands with summary table
|
|
13
13
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
14
|
-
def self.list_brands_with_counts(detailed: false)
|
|
14
|
+
def self.list_brands_with_counts(detailed: false, s3: false)
|
|
15
15
|
brands = Config.available_brands
|
|
16
16
|
|
|
17
17
|
if brands.empty?
|
|
@@ -19,64 +19,113 @@ module Appydave
|
|
|
19
19
|
return
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
# Gather brand data
|
|
23
|
-
brand_data = brands.map { |brand| collect_brand_data(brand, detailed: detailed) }
|
|
22
|
+
# Gather brand data (skip S3 if not requested)
|
|
23
|
+
brand_data = brands.map { |brand| collect_brand_data(brand, detailed: detailed, s3: s3) }
|
|
24
24
|
|
|
25
25
|
if detailed
|
|
26
26
|
# Detailed view with additional columns
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
if s3
|
|
28
|
+
header = 'BRAND KEY PROJECTS SIZE LAST MODIFIED ' \
|
|
29
|
+
'GIT S3 SYNC PATH SSD BACKUP ' \
|
|
30
|
+
'WORKFLOW ACTIVE'
|
|
31
|
+
puts header
|
|
32
|
+
puts '-' * 200
|
|
33
|
+
else
|
|
34
|
+
header = 'BRAND KEY PROJECTS SIZE LAST MODIFIED ' \
|
|
35
|
+
'GIT PATH SSD BACKUP ' \
|
|
36
|
+
'WORKFLOW ACTIVE'
|
|
37
|
+
puts header
|
|
38
|
+
puts '-' * 189
|
|
39
|
+
end
|
|
32
40
|
|
|
33
41
|
brand_data.each do |data|
|
|
34
42
|
brand_display = "#{data[:shortcut]} - #{data[:name]}"
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
if s3
|
|
45
|
+
puts format(
|
|
46
|
+
'%-30s %-12s %10d %12s %20s %-15s %-10s %-35s %-30s %-10s %6d',
|
|
47
|
+
brand_display,
|
|
48
|
+
data[:key],
|
|
49
|
+
data[:count],
|
|
50
|
+
format_size(data[:size]),
|
|
51
|
+
format_date(data[:modified]),
|
|
52
|
+
data[:git_status],
|
|
53
|
+
data[:s3_sync],
|
|
54
|
+
shorten_path(data[:path]),
|
|
55
|
+
data[:ssd_backup] || 'N/A',
|
|
56
|
+
data[:workflow] || 'N/A',
|
|
57
|
+
data[:active_count] || 0
|
|
58
|
+
)
|
|
59
|
+
else
|
|
60
|
+
puts format(
|
|
61
|
+
'%-30s %-12s %10d %12s %20s %-15s %-35s %-30s %-10s %6d',
|
|
62
|
+
brand_display,
|
|
63
|
+
data[:key],
|
|
64
|
+
data[:count],
|
|
65
|
+
format_size(data[:size]),
|
|
66
|
+
format_date(data[:modified]),
|
|
67
|
+
data[:git_status],
|
|
68
|
+
shorten_path(data[:path]),
|
|
69
|
+
data[:ssd_backup] || 'N/A',
|
|
70
|
+
data[:workflow] || 'N/A',
|
|
71
|
+
data[:active_count] || 0
|
|
72
|
+
)
|
|
73
|
+
end
|
|
50
74
|
end
|
|
51
75
|
else
|
|
52
76
|
# Default view - use same format for header and data
|
|
53
77
|
# rubocop:disable Style/RedundantFormat
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
if s3
|
|
79
|
+
puts format(
|
|
80
|
+
'%-30s %-15s %10s %12s %20s %-15s %-10s',
|
|
81
|
+
'BRAND',
|
|
82
|
+
'KEY',
|
|
83
|
+
'PROJECTS',
|
|
84
|
+
'SIZE',
|
|
85
|
+
'LAST MODIFIED',
|
|
86
|
+
'GIT',
|
|
87
|
+
'S3 SYNC'
|
|
88
|
+
)
|
|
89
|
+
puts '-' * 133
|
|
90
|
+
else
|
|
91
|
+
puts format(
|
|
92
|
+
'%-30s %-15s %10s %12s %20s %-15s',
|
|
93
|
+
'BRAND',
|
|
94
|
+
'KEY',
|
|
95
|
+
'PROJECTS',
|
|
96
|
+
'SIZE',
|
|
97
|
+
'LAST MODIFIED',
|
|
98
|
+
'GIT'
|
|
99
|
+
)
|
|
100
|
+
puts '-' * 122
|
|
101
|
+
end
|
|
64
102
|
# rubocop:enable Style/RedundantFormat
|
|
65
|
-
puts '-' * 133
|
|
66
103
|
|
|
67
104
|
brand_data.each do |data|
|
|
68
105
|
brand_display = "#{data[:shortcut]} - #{data[:name]}"
|
|
69
106
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
107
|
+
if s3
|
|
108
|
+
puts format(
|
|
109
|
+
'%-30s %-15s %10d %12s %20s %-15s %-10s',
|
|
110
|
+
brand_display,
|
|
111
|
+
data[:key],
|
|
112
|
+
data[:count],
|
|
113
|
+
format_size(data[:size]),
|
|
114
|
+
format_date(data[:modified]),
|
|
115
|
+
data[:git_status],
|
|
116
|
+
data[:s3_sync]
|
|
117
|
+
)
|
|
118
|
+
else
|
|
119
|
+
puts format(
|
|
120
|
+
'%-30s %-15s %10d %12s %20s %-15s',
|
|
121
|
+
brand_display,
|
|
122
|
+
data[:key],
|
|
123
|
+
data[:count],
|
|
124
|
+
format_size(data[:size]),
|
|
125
|
+
format_date(data[:modified]),
|
|
126
|
+
data[:git_status]
|
|
127
|
+
)
|
|
128
|
+
end
|
|
80
129
|
end
|
|
81
130
|
end
|
|
82
131
|
|
|
@@ -93,7 +142,7 @@ module Appydave
|
|
|
93
142
|
|
|
94
143
|
# List all projects for a specific brand (Mode 3)
|
|
95
144
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
96
|
-
def self.list_brand_projects(brand_arg, detailed: false)
|
|
145
|
+
def self.list_brand_projects(brand_arg, detailed: false, s3: false)
|
|
97
146
|
# ProjectResolver expects the original brand key/shortcut, not the expanded v-* version
|
|
98
147
|
projects = ProjectResolver.list_projects(brand_arg)
|
|
99
148
|
|
|
@@ -114,13 +163,13 @@ module Appydave
|
|
|
114
163
|
return
|
|
115
164
|
end
|
|
116
165
|
|
|
117
|
-
# Gather project data
|
|
166
|
+
# Gather project data (skip S3 if not requested)
|
|
118
167
|
brand_path = Config.brand_path(brand_arg)
|
|
119
168
|
brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(brand_arg)
|
|
120
169
|
is_git_repo = Dir.exist?(File.join(brand_path, '.git'))
|
|
121
170
|
|
|
122
171
|
project_data = projects.map do |project|
|
|
123
|
-
collect_project_data(brand_arg, brand_path, brand_info, project, is_git_repo, detailed: detailed)
|
|
172
|
+
collect_project_data(brand_arg, brand_path, brand_info, project, is_git_repo, detailed: detailed, s3: s3)
|
|
124
173
|
end
|
|
125
174
|
|
|
126
175
|
# Print common header
|
|
@@ -132,67 +181,119 @@ module Appydave
|
|
|
132
181
|
if detailed
|
|
133
182
|
# Detailed view with additional columns - use same format for header and data
|
|
134
183
|
# rubocop:disable Style/RedundantFormat
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
184
|
+
if s3
|
|
185
|
+
puts format(
|
|
186
|
+
'%-45s %12s %15s %-15s %-12s %-65s %-18s %-18s %-30s %-15s %-15s',
|
|
187
|
+
'PROJECT',
|
|
188
|
+
'SIZE',
|
|
189
|
+
'AGE',
|
|
190
|
+
'GIT',
|
|
191
|
+
'S3',
|
|
192
|
+
'PATH',
|
|
193
|
+
'HEAVY FILES',
|
|
194
|
+
'LIGHT FILES',
|
|
195
|
+
'SSD BACKUP',
|
|
196
|
+
'S3 ↑ UPLOAD',
|
|
197
|
+
'S3 ↓ DOWNLOAD'
|
|
198
|
+
)
|
|
199
|
+
puts '-' * 280
|
|
200
|
+
else
|
|
201
|
+
puts format(
|
|
202
|
+
'%-45s %12s %15s %-15s %-65s %-18s %-18s %-30s',
|
|
203
|
+
'PROJECT',
|
|
204
|
+
'SIZE',
|
|
205
|
+
'AGE',
|
|
206
|
+
'GIT',
|
|
207
|
+
'PATH',
|
|
208
|
+
'HEAVY FILES',
|
|
209
|
+
'LIGHT FILES',
|
|
210
|
+
'SSD BACKUP'
|
|
211
|
+
)
|
|
212
|
+
puts '-' * 239
|
|
213
|
+
end
|
|
149
214
|
# rubocop:enable Style/RedundantFormat
|
|
150
|
-
puts '-' * 280
|
|
151
215
|
|
|
152
216
|
project_data.each do |data|
|
|
153
217
|
age_display = data[:stale] ? "#{data[:age]} ⚠️" : data[:age]
|
|
154
|
-
s3_upload = data[:s3_last_upload] ? format_age(data[:s3_last_upload]) : 'N/A'
|
|
155
|
-
s3_download = data[:s3_last_download] ? format_age(data[:s3_last_download]) : 'N/A'
|
|
156
218
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
data[:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
219
|
+
if s3
|
|
220
|
+
s3_upload = data[:s3_last_upload] ? format_age(data[:s3_last_upload]) : 'N/A'
|
|
221
|
+
s3_download = data[:s3_last_download] ? format_age(data[:s3_last_download]) : 'N/A'
|
|
222
|
+
|
|
223
|
+
puts format(
|
|
224
|
+
'%-45s %12s %15s %-15s %-12s %-65s %-18s %-18s %-30s %-15s %-15s',
|
|
225
|
+
data[:name],
|
|
226
|
+
format_size(data[:size]),
|
|
227
|
+
age_display,
|
|
228
|
+
data[:git_status],
|
|
229
|
+
data[:s3_sync],
|
|
230
|
+
shorten_path(data[:path]),
|
|
231
|
+
data[:heavy_files] || 'N/A',
|
|
232
|
+
data[:light_files] || 'N/A',
|
|
233
|
+
data[:ssd_backup] || 'N/A',
|
|
234
|
+
s3_upload,
|
|
235
|
+
s3_download
|
|
236
|
+
)
|
|
237
|
+
else
|
|
238
|
+
puts format(
|
|
239
|
+
'%-45s %12s %15s %-15s %-65s %-18s %-18s %-30s',
|
|
240
|
+
data[:name],
|
|
241
|
+
format_size(data[:size]),
|
|
242
|
+
age_display,
|
|
243
|
+
data[:git_status],
|
|
244
|
+
shorten_path(data[:path]),
|
|
245
|
+
data[:heavy_files] || 'N/A',
|
|
246
|
+
data[:light_files] || 'N/A',
|
|
247
|
+
data[:ssd_backup] || 'N/A'
|
|
248
|
+
)
|
|
249
|
+
end
|
|
171
250
|
end
|
|
172
251
|
else
|
|
173
252
|
# Default view - use same format for header and data
|
|
174
253
|
# rubocop:disable Style/RedundantFormat
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
254
|
+
if s3
|
|
255
|
+
puts format(
|
|
256
|
+
'%-45s %12s %15s %-15s %-12s',
|
|
257
|
+
'PROJECT',
|
|
258
|
+
'SIZE',
|
|
259
|
+
'AGE',
|
|
260
|
+
'GIT',
|
|
261
|
+
'S3'
|
|
262
|
+
)
|
|
263
|
+
puts '-' * 130
|
|
264
|
+
else
|
|
265
|
+
puts format(
|
|
266
|
+
'%-45s %12s %15s %-15s',
|
|
267
|
+
'PROJECT',
|
|
268
|
+
'SIZE',
|
|
269
|
+
'AGE',
|
|
270
|
+
'GIT'
|
|
271
|
+
)
|
|
272
|
+
puts '-' * 117
|
|
273
|
+
end
|
|
183
274
|
# rubocop:enable Style/RedundantFormat
|
|
184
|
-
puts '-' * 130
|
|
185
275
|
|
|
186
276
|
project_data.each do |data|
|
|
187
277
|
age_display = data[:stale] ? "#{data[:age]} ⚠️" : data[:age]
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
278
|
+
|
|
279
|
+
if s3
|
|
280
|
+
puts format(
|
|
281
|
+
'%-45s %12s %15s %-15s %-12s',
|
|
282
|
+
data[:name],
|
|
283
|
+
format_size(data[:size]),
|
|
284
|
+
age_display,
|
|
285
|
+
data[:git_status],
|
|
286
|
+
data[:s3_sync]
|
|
287
|
+
)
|
|
288
|
+
else
|
|
289
|
+
puts format(
|
|
290
|
+
'%-45s %12s %15s %-15s',
|
|
291
|
+
data[:name],
|
|
292
|
+
format_size(data[:size]),
|
|
293
|
+
age_display,
|
|
294
|
+
data[:git_status]
|
|
295
|
+
)
|
|
296
|
+
end
|
|
196
297
|
end
|
|
197
298
|
end
|
|
198
299
|
|
|
@@ -313,7 +414,7 @@ module Appydave
|
|
|
313
414
|
|
|
314
415
|
# Collect brand data for display
|
|
315
416
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
316
|
-
def self.collect_brand_data(brand, detailed: false)
|
|
417
|
+
def self.collect_brand_data(brand, detailed: false, s3: false)
|
|
317
418
|
Appydave::Tools::Configuration::Config.configure
|
|
318
419
|
brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(brand)
|
|
319
420
|
brand_path = Config.brand_path(brand)
|
|
@@ -331,8 +432,8 @@ module Appydave
|
|
|
331
432
|
# Get git status
|
|
332
433
|
git_status = calculate_git_status(brand_path)
|
|
333
434
|
|
|
334
|
-
# Get S3 sync status (count of projects with s3-staging)
|
|
335
|
-
s3_sync_status = calculate_s3_sync_status(brand, projects)
|
|
435
|
+
# Get S3 sync status (count of projects with s3-staging) - only if requested
|
|
436
|
+
s3_sync_status = s3 ? calculate_s3_sync_status(brand, projects) : 'N/A'
|
|
336
437
|
|
|
337
438
|
result = {
|
|
338
439
|
shortcut: shortcut || key,
|
|
@@ -449,7 +550,7 @@ module Appydave
|
|
|
449
550
|
|
|
450
551
|
# Collect project data for display
|
|
451
552
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/ParameterLists
|
|
452
|
-
def self.collect_project_data(brand_arg, brand_path, brand_info, project, is_git_repo, detailed: false)
|
|
553
|
+
def self.collect_project_data(brand_arg, brand_path, brand_info, project, is_git_repo, detailed: false, s3: false)
|
|
453
554
|
project_path = Config.project_path(brand_arg, project)
|
|
454
555
|
size = FileHelper.calculate_directory_size(project_path)
|
|
455
556
|
modified = File.mtime(project_path)
|
|
@@ -461,8 +562,8 @@ module Appydave
|
|
|
461
562
|
'N/A'
|
|
462
563
|
end
|
|
463
564
|
|
|
464
|
-
# Calculate 3-state S3 sync status
|
|
465
|
-
s3_sync = calculate_project_s3_sync_status(brand_arg, brand_info, project)
|
|
565
|
+
# Calculate 3-state S3 sync status - only if requested (performance optimization)
|
|
566
|
+
s3_sync = s3 ? calculate_project_s3_sync_status(brand_arg, brand_info, project) : 'N/A'
|
|
466
567
|
|
|
467
568
|
result = {
|
|
468
569
|
name: project,
|
|
@@ -500,16 +601,23 @@ module Appydave
|
|
|
500
601
|
File.exist?(ssd_project_path) ? shorten_path(ssd_project_path) : nil
|
|
501
602
|
end
|
|
502
603
|
|
|
503
|
-
# S3 timestamps (last upload/download)
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
604
|
+
# S3 timestamps (last upload/download) - only if requested (performance optimization)
|
|
605
|
+
if s3
|
|
606
|
+
s3_timestamps = calculate_s3_timestamps(brand_arg, brand_info, project)
|
|
607
|
+
result.merge!(
|
|
608
|
+
heavy_files: "#{heavy_count} (#{format_size(heavy_size)})",
|
|
609
|
+
light_files: "#{light_count} (#{format_size(light_size)})",
|
|
610
|
+
ssd_backup: ssd_path,
|
|
611
|
+
s3_last_upload: s3_timestamps[:last_upload],
|
|
612
|
+
s3_last_download: s3_timestamps[:last_download]
|
|
613
|
+
)
|
|
614
|
+
else
|
|
615
|
+
result.merge!(
|
|
616
|
+
heavy_files: "#{heavy_count} (#{format_size(heavy_size)})",
|
|
617
|
+
light_files: "#{light_count} (#{format_size(light_size)})",
|
|
618
|
+
ssd_backup: ssd_path
|
|
619
|
+
)
|
|
620
|
+
end
|
|
513
621
|
end
|
|
514
622
|
|
|
515
623
|
result
|
|
@@ -152,35 +152,37 @@ module Appydave
|
|
|
152
152
|
|
|
153
153
|
s3_path = build_s3_key(relative_path)
|
|
154
154
|
|
|
155
|
-
# Check if file already exists
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
puts "
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
s3_file_info = get_s3_file_info(s3_path)
|
|
169
|
-
if s3_file_info && s3_file_info['LastModified']
|
|
170
|
-
s3_time = s3_file_info['LastModified']
|
|
171
|
-
local_time = File.mtime(file)
|
|
172
|
-
puts " S3: #{s3_time.strftime('%Y-%m-%d %H:%M')} | Local: #{local_time.strftime('%Y-%m-%d %H:%M')}"
|
|
173
|
-
|
|
174
|
-
puts ' ⚠️ S3 file is NEWER than local - you may be overwriting recent changes!' if s3_time > local_time
|
|
175
|
-
end
|
|
176
|
-
puts ' Uploading will overwrite S3 version...'
|
|
155
|
+
# Check if file already exists in S3 and compare
|
|
156
|
+
s3_info = get_s3_file_info(s3_path)
|
|
157
|
+
|
|
158
|
+
if s3_info
|
|
159
|
+
s3_etag = s3_info['ETag'].gsub('"', '')
|
|
160
|
+
s3_size = s3_info['Size']
|
|
161
|
+
match_status = compare_files(local_file: file, s3_etag: s3_etag, s3_size: s3_size)
|
|
162
|
+
|
|
163
|
+
if match_status == :synced
|
|
164
|
+
comparison_method = multipart_etag?(s3_etag) ? 'size match' : 'unchanged'
|
|
165
|
+
puts " ⏭️ Skipped: #{relative_path} (#{comparison_method})"
|
|
166
|
+
skipped += 1
|
|
167
|
+
next
|
|
177
168
|
end
|
|
178
169
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
170
|
+
# File exists but content differs - warn before overwriting
|
|
171
|
+
puts " ⚠️ Warning: #{relative_path} exists in S3 with different content"
|
|
172
|
+
puts ' (multipart upload detected - comparing by size)' if multipart_etag?(s3_etag)
|
|
173
|
+
|
|
174
|
+
s3_time = s3_info['LastModified']
|
|
175
|
+
local_time = File.mtime(file)
|
|
176
|
+
puts " S3: #{s3_time.strftime('%Y-%m-%d %H:%M')} | Local: #{local_time.strftime('%Y-%m-%d %H:%M')}"
|
|
177
|
+
|
|
178
|
+
puts ' ⚠️ S3 file is NEWER than local - you may be overwriting recent changes!' if s3_time > local_time
|
|
179
|
+
puts ' Uploading will overwrite S3 version...'
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if upload_file(file, s3_path, dry_run: dry_run)
|
|
183
|
+
uploaded += 1
|
|
184
|
+
else
|
|
185
|
+
failed += 1
|
|
184
186
|
end
|
|
185
187
|
end
|
|
186
188
|
# rubocop:enable Metrics/BlockLength
|
|
@@ -215,41 +217,47 @@ module Appydave
|
|
|
215
217
|
skipped = 0
|
|
216
218
|
failed = 0
|
|
217
219
|
|
|
220
|
+
# rubocop:disable Metrics/BlockLength
|
|
218
221
|
s3_files.each do |s3_file|
|
|
219
222
|
key = s3_file['Key']
|
|
220
223
|
relative_path = extract_relative_path(key)
|
|
221
224
|
local_file = File.join(staging_dir, relative_path)
|
|
222
225
|
|
|
223
|
-
# Check if file already exists
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
# Check if file already exists and compare
|
|
227
|
+
s3_etag = s3_file['ETag'].gsub('"', '')
|
|
228
|
+
s3_size = s3_file['Size']
|
|
226
229
|
|
|
227
|
-
if
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
# Compare timestamps
|
|
236
|
-
if s3_file['LastModified'] && File.exist?(local_file)
|
|
237
|
-
s3_time = s3_file['LastModified']
|
|
238
|
-
local_time = File.mtime(local_file)
|
|
239
|
-
puts " S3: #{s3_time.strftime('%Y-%m-%d %H:%M')} | Local: #{local_time.strftime('%Y-%m-%d %H:%M')}"
|
|
240
|
-
|
|
241
|
-
puts ' ⚠️ Local file is NEWER than S3 - you may be overwriting recent changes!' if local_time > s3_time
|
|
242
|
-
end
|
|
243
|
-
puts ' Downloading will overwrite local version...'
|
|
230
|
+
if File.exist?(local_file)
|
|
231
|
+
match_status = compare_files(local_file: local_file, s3_etag: s3_etag, s3_size: s3_size)
|
|
232
|
+
|
|
233
|
+
if match_status == :synced
|
|
234
|
+
comparison_method = multipart_etag?(s3_etag) ? 'size match' : 'unchanged'
|
|
235
|
+
puts " ⏭️ Skipped: #{relative_path} (#{comparison_method})"
|
|
236
|
+
skipped += 1
|
|
237
|
+
next
|
|
244
238
|
end
|
|
245
239
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
240
|
+
# File exists but content differs - warn before overwriting
|
|
241
|
+
puts " ⚠️ Warning: #{relative_path} exists locally with different content"
|
|
242
|
+
puts ' (multipart upload detected - comparing by size)' if multipart_etag?(s3_etag)
|
|
243
|
+
|
|
244
|
+
if s3_file['LastModified']
|
|
245
|
+
s3_time = s3_file['LastModified']
|
|
246
|
+
local_time = File.mtime(local_file)
|
|
247
|
+
puts " S3: #{s3_time.strftime('%Y-%m-%d %H:%M')} | Local: #{local_time.strftime('%Y-%m-%d %H:%M')}"
|
|
248
|
+
|
|
249
|
+
puts ' ⚠️ Local file is NEWER than S3 - you may be overwriting recent changes!' if local_time > s3_time
|
|
250
250
|
end
|
|
251
|
+
puts ' Downloading will overwrite local version...'
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
if download_file(key, local_file, dry_run: dry_run)
|
|
255
|
+
downloaded += 1
|
|
256
|
+
else
|
|
257
|
+
failed += 1
|
|
251
258
|
end
|
|
252
259
|
end
|
|
260
|
+
# rubocop:enable Metrics/BlockLength
|
|
253
261
|
puts ''
|
|
254
262
|
puts '✅ Download complete!'
|
|
255
263
|
puts " Downloaded: #{downloaded}, Skipped: #{skipped}, Failed: #{failed}"
|
|
@@ -322,11 +330,12 @@ module Appydave
|
|
|
322
330
|
total_s3_size += s3_size
|
|
323
331
|
total_local_size += local_size
|
|
324
332
|
|
|
325
|
-
|
|
326
|
-
|
|
333
|
+
s3_etag = s3_file['ETag'].gsub('"', '')
|
|
334
|
+
match_status = compare_files(local_file: local_file, s3_etag: s3_etag, s3_size: s3_size)
|
|
327
335
|
|
|
328
|
-
if
|
|
329
|
-
|
|
336
|
+
if match_status == :synced
|
|
337
|
+
status_label = multipart_etag?(s3_etag) ? 'synced*' : 'synced'
|
|
338
|
+
puts " ✓ #{relative_path} (#{file_size_human(s3_size)}) [#{status_label}]"
|
|
330
339
|
else
|
|
331
340
|
puts " ⚠️ #{relative_path} (#{file_size_human(s3_size)}) [modified]"
|
|
332
341
|
end
|
|
@@ -526,10 +535,11 @@ module Appydave
|
|
|
526
535
|
s3_file = s3_files_map[relative_path]
|
|
527
536
|
|
|
528
537
|
if s3_file
|
|
529
|
-
# Compare
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
538
|
+
# Compare using multipart-aware comparison
|
|
539
|
+
s3_etag = s3_file['ETag'].gsub('"', '')
|
|
540
|
+
s3_size = s3_file['Size']
|
|
541
|
+
match_status = compare_files(local_file: local_file, s3_etag: s3_etag, s3_size: s3_size)
|
|
542
|
+
needs_upload = true if match_status != :synced
|
|
533
543
|
else
|
|
534
544
|
# Local file not in S3
|
|
535
545
|
needs_upload = true
|
|
@@ -623,6 +633,47 @@ module Appydave
|
|
|
623
633
|
nil
|
|
624
634
|
end
|
|
625
635
|
|
|
636
|
+
# Check if an S3 ETag is from a multipart upload
|
|
637
|
+
# Multipart ETags have format: "hash-partcount" (e.g., "d41d8cd98f00b204e9800998ecf8427e-5")
|
|
638
|
+
def multipart_etag?(etag)
|
|
639
|
+
return false if etag.nil?
|
|
640
|
+
|
|
641
|
+
etag.include?('-')
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Compare local file with S3 file, handling multipart ETags
|
|
645
|
+
# Returns: :synced, :modified, or :unknown
|
|
646
|
+
# For multipart uploads, falls back to size comparison since MD5 won't match
|
|
647
|
+
def compare_files(local_file:, s3_etag:, s3_size:)
|
|
648
|
+
return :unknown unless File.exist?(local_file)
|
|
649
|
+
return :unknown if s3_etag.nil?
|
|
650
|
+
|
|
651
|
+
local_size = File.size(local_file)
|
|
652
|
+
|
|
653
|
+
if multipart_etag?(s3_etag)
|
|
654
|
+
# Multipart upload - MD5 comparison won't work, use size
|
|
655
|
+
# Size match is a reasonable proxy for "unchanged" in this context
|
|
656
|
+
local_size == s3_size ? :synced : :modified
|
|
657
|
+
else
|
|
658
|
+
# Standard upload - use MD5 comparison
|
|
659
|
+
local_md5 = file_md5(local_file)
|
|
660
|
+
return :unknown if local_md5.nil?
|
|
661
|
+
|
|
662
|
+
local_md5 == s3_etag ? :synced : :modified
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# Get S3 file size from path (for upload comparison)
|
|
667
|
+
def s3_file_size(s3_path)
|
|
668
|
+
response = s3_client.head_object(
|
|
669
|
+
bucket: brand_info.aws.s3_bucket,
|
|
670
|
+
key: s3_path
|
|
671
|
+
)
|
|
672
|
+
response.content_length
|
|
673
|
+
rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::ServiceError
|
|
674
|
+
nil
|
|
675
|
+
end
|
|
676
|
+
|
|
626
677
|
# Upload file to S3
|
|
627
678
|
def upload_file(local_file, s3_path, dry_run: false)
|
|
628
679
|
if dry_run
|