appydave-tools 0.77.1 → 0.77.3
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/CHANGELOG.md +14 -0
- data/docs/planning/BACKLOG.md +9 -6
- data/docs/planning/batch-a-features/IMPLEMENTATION_PLAN.md +5 -5
- data/docs/planning/s3-operations-split/AGENTS.md +686 -0
- data/docs/planning/s3-operations-split/IMPLEMENTATION_PLAN.md +42 -0
- data/lib/appydave/tools/dam/s3_base.rb +310 -0
- data/lib/appydave/tools/dam/s3_operations.rb +18 -468
- data/lib/appydave/tools/dam/s3_uploader.rb +171 -0
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools.rb +2 -0
- data/package.json +1 -1
- metadata +5 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appydave
|
|
4
|
+
module Tools
|
|
5
|
+
module Dam
|
|
6
|
+
# Handles S3 upload operations.
|
|
7
|
+
# Inherits shared infrastructure and helpers from S3Base.
|
|
8
|
+
class S3Uploader < S3Base
|
|
9
|
+
def upload(dry_run: false)
|
|
10
|
+
project_dir = project_directory_path
|
|
11
|
+
staging_dir = File.join(project_dir, 's3-staging')
|
|
12
|
+
|
|
13
|
+
unless Dir.exist?(staging_dir)
|
|
14
|
+
puts "❌ No s3-staging directory found: #{staging_dir}"
|
|
15
|
+
puts 'Nothing to upload.'
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
files = Dir.glob("#{staging_dir}/**/*").select { |f| File.file?(f) }
|
|
20
|
+
|
|
21
|
+
if files.empty?
|
|
22
|
+
puts '❌ No files found in s3-staging/'
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
puts "📦 Uploading #{files.size} file(s) from #{project_id}/s3-staging/ to S3..."
|
|
27
|
+
puts ''
|
|
28
|
+
|
|
29
|
+
uploaded = 0
|
|
30
|
+
skipped = 0
|
|
31
|
+
failed = 0
|
|
32
|
+
|
|
33
|
+
# rubocop:disable Metrics/BlockLength
|
|
34
|
+
files.each do |file|
|
|
35
|
+
relative_path = file.sub("#{staging_dir}/", '')
|
|
36
|
+
|
|
37
|
+
# Skip excluded files (e.g., Windows Zone.Identifier, .DS_Store)
|
|
38
|
+
if excluded_path?(relative_path)
|
|
39
|
+
skipped += 1
|
|
40
|
+
next
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
s3_path = build_s3_key(relative_path)
|
|
44
|
+
|
|
45
|
+
# Check if file already exists in S3 and compare
|
|
46
|
+
s3_info = get_s3_file_info(s3_path)
|
|
47
|
+
|
|
48
|
+
if s3_info
|
|
49
|
+
s3_etag = s3_info['ETag'].gsub('"', '')
|
|
50
|
+
s3_size = s3_info['Size']
|
|
51
|
+
match_status = compare_files(local_file: file, s3_etag: s3_etag, s3_size: s3_size)
|
|
52
|
+
|
|
53
|
+
if match_status == :synced
|
|
54
|
+
comparison_method = multipart_etag?(s3_etag) ? 'size match' : 'unchanged'
|
|
55
|
+
puts " ⏭️ Skipped: #{relative_path} (#{comparison_method})"
|
|
56
|
+
skipped += 1
|
|
57
|
+
next
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# File exists but content differs - warn before overwriting
|
|
61
|
+
puts " ⚠️ Warning: #{relative_path} exists in S3 with different content"
|
|
62
|
+
puts ' (multipart upload detected - comparing by size)' if multipart_etag?(s3_etag)
|
|
63
|
+
|
|
64
|
+
s3_time = s3_info['LastModified']
|
|
65
|
+
local_time = File.mtime(file)
|
|
66
|
+
puts " S3: #{s3_time.strftime('%Y-%m-%d %H:%M')} | Local: #{local_time.strftime('%Y-%m-%d %H:%M')}"
|
|
67
|
+
|
|
68
|
+
puts ' ⚠️ S3 file is NEWER than local - you may be overwriting recent changes!' if s3_time > local_time
|
|
69
|
+
puts ' Uploading will overwrite S3 version...'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if upload_file(file, s3_path, dry_run: dry_run)
|
|
73
|
+
uploaded += 1
|
|
74
|
+
else
|
|
75
|
+
failed += 1
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
# rubocop:enable Metrics/BlockLength
|
|
79
|
+
|
|
80
|
+
puts ''
|
|
81
|
+
puts '✅ Upload complete!'
|
|
82
|
+
puts " Uploaded: #{uploaded}, Skipped: #{skipped}, Failed: #{failed}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def upload_file(local_file, s3_path, dry_run: false)
|
|
88
|
+
if dry_run
|
|
89
|
+
puts " [DRY-RUN] Would upload: #{local_file} → s3://#{brand_info.aws.s3_bucket}/#{s3_path}"
|
|
90
|
+
return true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Detect MIME type for proper browser handling
|
|
94
|
+
content_type = detect_content_type(local_file)
|
|
95
|
+
|
|
96
|
+
# For large files, use TransferManager for managed uploads (supports multipart)
|
|
97
|
+
file_size = File.size(local_file)
|
|
98
|
+
start_time = Time.now
|
|
99
|
+
|
|
100
|
+
if file_size > 100 * 1024 * 1024 # > 100MB
|
|
101
|
+
puts " 📤 Uploading large file (#{file_size_human(file_size)})..."
|
|
102
|
+
|
|
103
|
+
# Use TransferManager for multipart upload (modern AWS SDK approach)
|
|
104
|
+
transfer_manager = Aws::S3::TransferManager.new(client: s3_client)
|
|
105
|
+
transfer_manager.upload_file(
|
|
106
|
+
local_file,
|
|
107
|
+
bucket: brand_info.aws.s3_bucket,
|
|
108
|
+
key: s3_path,
|
|
109
|
+
content_type: content_type
|
|
110
|
+
)
|
|
111
|
+
else
|
|
112
|
+
# For smaller files, use direct put_object
|
|
113
|
+
File.open(local_file, 'rb') do |file|
|
|
114
|
+
s3_client.put_object(
|
|
115
|
+
bucket: brand_info.aws.s3_bucket,
|
|
116
|
+
key: s3_path,
|
|
117
|
+
body: file,
|
|
118
|
+
content_type: content_type
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
elapsed = Time.now - start_time
|
|
124
|
+
elapsed_str = format_duration(elapsed)
|
|
125
|
+
puts " ✓ Uploaded: #{File.basename(local_file)} (#{file_size_human(file_size)}) in #{elapsed_str}"
|
|
126
|
+
true
|
|
127
|
+
rescue Aws::S3::Errors::ServiceError => e
|
|
128
|
+
puts " ✗ Failed: #{File.basename(local_file)}"
|
|
129
|
+
puts " Error: #{e.message}"
|
|
130
|
+
false
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
puts " ✗ Failed: #{File.basename(local_file)}"
|
|
133
|
+
puts " Error: #{e.class} - #{e.message}"
|
|
134
|
+
false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def detect_content_type(filename)
|
|
138
|
+
ext = File.extname(filename).downcase
|
|
139
|
+
case ext
|
|
140
|
+
when '.mp4'
|
|
141
|
+
'video/mp4'
|
|
142
|
+
when '.mov'
|
|
143
|
+
'video/quicktime'
|
|
144
|
+
when '.avi'
|
|
145
|
+
'video/x-msvideo'
|
|
146
|
+
when '.mkv'
|
|
147
|
+
'video/x-matroska'
|
|
148
|
+
when '.webm'
|
|
149
|
+
'video/webm'
|
|
150
|
+
when '.m4v'
|
|
151
|
+
'video/x-m4v'
|
|
152
|
+
when '.jpg', '.jpeg'
|
|
153
|
+
'image/jpeg'
|
|
154
|
+
when '.png'
|
|
155
|
+
'image/png'
|
|
156
|
+
when '.gif'
|
|
157
|
+
'image/gif'
|
|
158
|
+
when '.pdf'
|
|
159
|
+
'application/pdf'
|
|
160
|
+
when '.json'
|
|
161
|
+
'application/json'
|
|
162
|
+
when '.srt', '.vtt', '.txt', '.md'
|
|
163
|
+
'text/plain'
|
|
164
|
+
else
|
|
165
|
+
'application/octet-stream'
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
data/lib/appydave/tools.rb
CHANGED
|
@@ -67,6 +67,8 @@ require 'appydave/tools/dam/brand_resolver'
|
|
|
67
67
|
require 'appydave/tools/dam/config'
|
|
68
68
|
require 'appydave/tools/dam/project_resolver'
|
|
69
69
|
require 'appydave/tools/dam/config_loader'
|
|
70
|
+
require 'appydave/tools/dam/s3_base'
|
|
71
|
+
require 'appydave/tools/dam/s3_uploader'
|
|
70
72
|
require 'appydave/tools/dam/s3_operations'
|
|
71
73
|
require 'appydave/tools/dam/s3_scanner'
|
|
72
74
|
require 'appydave/tools/dam/share_operations'
|
data/package.json
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: appydave-tools
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.77.
|
|
4
|
+
version: 0.77.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cruwys
|
|
@@ -321,6 +321,8 @@ files:
|
|
|
321
321
|
- docs/planning/micro-cleanup/AGENTS.md
|
|
322
322
|
- docs/planning/micro-cleanup/IMPLEMENTATION_PLAN.md
|
|
323
323
|
- docs/planning/micro-cleanup/assessment.md
|
|
324
|
+
- docs/planning/s3-operations-split/AGENTS.md
|
|
325
|
+
- docs/planning/s3-operations-split/IMPLEMENTATION_PLAN.md
|
|
324
326
|
- docs/planning/test-coverage-gaps/AGENTS.md
|
|
325
327
|
- docs/planning/test-coverage-gaps/IMPLEMENTATION_PLAN.md
|
|
326
328
|
- docs/planning/test-coverage-gaps/assessment.md
|
|
@@ -372,9 +374,11 @@ files:
|
|
|
372
374
|
- lib/appydave/tools/dam/repo_status.rb
|
|
373
375
|
- lib/appydave/tools/dam/repo_sync.rb
|
|
374
376
|
- lib/appydave/tools/dam/s3_arg_parser.rb
|
|
377
|
+
- lib/appydave/tools/dam/s3_base.rb
|
|
375
378
|
- lib/appydave/tools/dam/s3_operations.rb
|
|
376
379
|
- lib/appydave/tools/dam/s3_scan_command.rb
|
|
377
380
|
- lib/appydave/tools/dam/s3_scanner.rb
|
|
381
|
+
- lib/appydave/tools/dam/s3_uploader.rb
|
|
378
382
|
- lib/appydave/tools/dam/share_operations.rb
|
|
379
383
|
- lib/appydave/tools/dam/ssd_status.rb
|
|
380
384
|
- lib/appydave/tools/dam/status.rb
|