appydave-tools 0.77.4 → 0.77.5
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 +7 -0
- data/lib/appydave/tools/dam/s3_operations.rb +3 -176
- data/lib/appydave/tools/dam/s3_status_checker.rb +178 -0
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools.rb +1 -0
- data/package.json +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a66f70a8ea85fce726ea3da189166f623c7fb1ca007f70b1b5fb75f3fd828077
|
|
4
|
+
data.tar.gz: 423a8d4309fb166084940efe6bcb7fbd3ab9d620094b006bf95194aef671719a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d89bf4f8992c1ab8630a51af56dc97d9fbc651605593be1b70510cc5dd4837472dfc55514d12d9ff0542b439ec04d0c47752a3799136b7ca4cada342d49aa023
|
|
7
|
+
data.tar.gz: 6fc875fe4006e621674af60652e5ff1241d5404f4d4bdd6e3374314e1460212bce2ec8cfc4129feab3a55ec18ae01fb33f08dc55679ad55d1efde3ed209a5bb0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [0.77.4](https://github.com/appydave/appydave-tools/compare/v0.77.3...v0.77.4) (2026-03-20)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* extract S3Downloader from S3Operations; download delegates to focused class ([4d97cdf](https://github.com/appydave/appydave-tools/commit/4d97cdf18ea56ca9b658c8858efd9041dcb7ca5b))
|
|
7
|
+
|
|
1
8
|
## [0.77.3](https://github.com/appydave/appydave-tools/compare/v0.77.2...v0.77.3) (2026-03-20)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -19,96 +19,7 @@ module Appydave
|
|
|
19
19
|
|
|
20
20
|
# Show sync status
|
|
21
21
|
def status
|
|
22
|
-
|
|
23
|
-
staging_dir = File.join(project_dir, 's3-staging')
|
|
24
|
-
|
|
25
|
-
# Check if project directory exists
|
|
26
|
-
unless Dir.exist?(project_dir)
|
|
27
|
-
puts "❌ Project not found: #{brand}/#{project_id}"
|
|
28
|
-
puts ''
|
|
29
|
-
puts ' This project does not exist locally.'
|
|
30
|
-
puts ' Possible causes:'
|
|
31
|
-
puts ' - Project name might be misspelled'
|
|
32
|
-
puts ' - Project may not exist in this brand'
|
|
33
|
-
puts ''
|
|
34
|
-
puts " Try: dam list #{brand} # See all projects for this brand"
|
|
35
|
-
return
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
s3_files = list_s3_files
|
|
39
|
-
local_files = list_local_files(staging_dir)
|
|
40
|
-
|
|
41
|
-
# Build a map of S3 files for quick lookup
|
|
42
|
-
s3_files_map = s3_files.each_with_object({}) do |file, hash|
|
|
43
|
-
relative_path = extract_relative_path(file['Key'])
|
|
44
|
-
hash[relative_path] = file
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
if s3_files.empty? && local_files.empty?
|
|
48
|
-
puts "ℹ️ No files in S3 or s3-staging/ for #{brand}/#{project_id}"
|
|
49
|
-
puts ''
|
|
50
|
-
puts ' This project exists but has no heavy files ready for S3 sync.'
|
|
51
|
-
puts ''
|
|
52
|
-
puts ' Next steps:'
|
|
53
|
-
puts " 1. Add video files to: #{staging_dir}/"
|
|
54
|
-
puts " 2. Upload to S3: dam s3-up #{brand} #{project_id}"
|
|
55
|
-
return
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
puts "📊 S3 Sync Status for #{brand}/#{project_id}"
|
|
59
|
-
|
|
60
|
-
# Show last sync time
|
|
61
|
-
if s3_files.any?
|
|
62
|
-
most_recent = s3_files.map { |f| f['LastModified'] }.compact.max
|
|
63
|
-
if most_recent
|
|
64
|
-
time_ago = format_time_ago(Time.now - most_recent)
|
|
65
|
-
puts " Last synced: #{time_ago} ago (#{most_recent.strftime('%Y-%m-%d %H:%M')})"
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
puts ''
|
|
69
|
-
|
|
70
|
-
# Combine all file paths (S3 + local)
|
|
71
|
-
all_paths = (s3_files_map.keys + local_files.keys).uniq.sort
|
|
72
|
-
|
|
73
|
-
total_s3_size = 0
|
|
74
|
-
total_local_size = 0
|
|
75
|
-
|
|
76
|
-
all_paths.each do |relative_path|
|
|
77
|
-
s3_file = s3_files_map[relative_path]
|
|
78
|
-
local_file = File.join(staging_dir, relative_path)
|
|
79
|
-
|
|
80
|
-
if s3_file && File.exist?(local_file)
|
|
81
|
-
# File exists in both S3 and local
|
|
82
|
-
s3_size = s3_file['Size']
|
|
83
|
-
local_size = File.size(local_file)
|
|
84
|
-
total_s3_size += s3_size
|
|
85
|
-
total_local_size += local_size
|
|
86
|
-
|
|
87
|
-
s3_etag = s3_file['ETag'].gsub('"', '')
|
|
88
|
-
match_status = compare_files(local_file: local_file, s3_etag: s3_etag, s3_size: s3_size)
|
|
89
|
-
|
|
90
|
-
if match_status == :synced
|
|
91
|
-
status_label = multipart_etag?(s3_etag) ? 'synced*' : 'synced'
|
|
92
|
-
puts " ✓ #{relative_path} (#{file_size_human(s3_size)}) [#{status_label}]"
|
|
93
|
-
else
|
|
94
|
-
puts " ⚠️ #{relative_path} (#{file_size_human(s3_size)}) [modified]"
|
|
95
|
-
end
|
|
96
|
-
elsif s3_file
|
|
97
|
-
# File only in S3
|
|
98
|
-
s3_size = s3_file['Size']
|
|
99
|
-
total_s3_size += s3_size
|
|
100
|
-
puts " ☁️ #{relative_path} (#{file_size_human(s3_size)}) [S3 only]"
|
|
101
|
-
else
|
|
102
|
-
# File only local
|
|
103
|
-
local_size = File.size(local_file)
|
|
104
|
-
total_local_size += local_size
|
|
105
|
-
puts " 📁 #{relative_path} (#{file_size_human(local_size)}) [local only]"
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
puts ''
|
|
110
|
-
puts "S3 files: #{s3_files.size}, Local files: #{local_files.size}"
|
|
111
|
-
puts "S3 size: #{file_size_human(total_s3_size)}, Local size: #{file_size_human(total_local_size)}"
|
|
22
|
+
S3StatusChecker.new(brand, project_id, **delegated_opts).status
|
|
112
23
|
end
|
|
113
24
|
|
|
114
25
|
# Cleanup S3 files
|
|
@@ -252,97 +163,13 @@ module Appydave
|
|
|
252
163
|
# Calculate 3-state S3 sync status
|
|
253
164
|
# @return [String] One of: '↑ upload', '↓ download', '✓ synced', 'none'
|
|
254
165
|
def calculate_sync_status
|
|
255
|
-
|
|
256
|
-
staging_dir = File.join(project_dir, 's3-staging')
|
|
257
|
-
|
|
258
|
-
# No s3-staging directory means no S3 intent
|
|
259
|
-
return 'none' unless Dir.exist?(staging_dir)
|
|
260
|
-
|
|
261
|
-
# Get S3 files (if S3 configured)
|
|
262
|
-
begin
|
|
263
|
-
s3_files = list_s3_files
|
|
264
|
-
rescue StandardError
|
|
265
|
-
# S3 not configured or not accessible
|
|
266
|
-
return 'none'
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
local_files = list_local_files(staging_dir)
|
|
270
|
-
|
|
271
|
-
# No files anywhere
|
|
272
|
-
return 'none' if s3_files.empty? && local_files.empty?
|
|
273
|
-
|
|
274
|
-
# Build S3 files map
|
|
275
|
-
s3_files_map = s3_files.each_with_object({}) do |file, hash|
|
|
276
|
-
relative_path = extract_relative_path(file['Key'])
|
|
277
|
-
hash[relative_path] = file
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
# Check for differences
|
|
281
|
-
needs_upload = false
|
|
282
|
-
needs_download = false
|
|
283
|
-
|
|
284
|
-
# Check all local files
|
|
285
|
-
local_files.each_key do |relative_path|
|
|
286
|
-
local_file = File.join(staging_dir, relative_path)
|
|
287
|
-
s3_file = s3_files_map[relative_path]
|
|
288
|
-
|
|
289
|
-
if s3_file
|
|
290
|
-
# Compare using multipart-aware comparison
|
|
291
|
-
s3_etag = s3_file['ETag'].gsub('"', '')
|
|
292
|
-
s3_size = s3_file['Size']
|
|
293
|
-
match_status = compare_files(local_file: local_file, s3_etag: s3_etag, s3_size: s3_size)
|
|
294
|
-
needs_upload = true if match_status != :synced
|
|
295
|
-
else
|
|
296
|
-
# Local file not in S3
|
|
297
|
-
needs_upload = true
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
# Check for S3-only files
|
|
302
|
-
s3_files_map.each_key do |relative_path|
|
|
303
|
-
local_file = File.join(staging_dir, relative_path)
|
|
304
|
-
needs_download = true unless File.exist?(local_file)
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
# Return status based on what's needed
|
|
308
|
-
if needs_upload && needs_download
|
|
309
|
-
'⚠️ both'
|
|
310
|
-
elsif needs_upload
|
|
311
|
-
'↑ upload'
|
|
312
|
-
elsif needs_download
|
|
313
|
-
'↓ download'
|
|
314
|
-
else
|
|
315
|
-
'✓ synced'
|
|
316
|
-
end
|
|
166
|
+
S3StatusChecker.new(brand, project_id, **delegated_opts).calculate_sync_status
|
|
317
167
|
end
|
|
318
168
|
|
|
319
169
|
# Calculate S3 sync timestamps (last upload/download times)
|
|
320
170
|
# @return [Hash] { last_upload: Time|nil, last_download: Time|nil }
|
|
321
171
|
def sync_timestamps
|
|
322
|
-
|
|
323
|
-
staging_dir = File.join(project_dir, 's3-staging')
|
|
324
|
-
|
|
325
|
-
# No s3-staging directory means no S3 intent
|
|
326
|
-
return { last_upload: nil, last_download: nil } unless Dir.exist?(staging_dir)
|
|
327
|
-
|
|
328
|
-
# Get S3 files (if S3 configured)
|
|
329
|
-
begin
|
|
330
|
-
s3_files = list_s3_files
|
|
331
|
-
rescue StandardError
|
|
332
|
-
# S3 not configured or not accessible
|
|
333
|
-
return { last_upload: nil, last_download: nil }
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
# Last upload time = most recent S3 file LastModified
|
|
337
|
-
last_upload = s3_files.map { |f| f['LastModified'] }.compact.max if s3_files.any?
|
|
338
|
-
|
|
339
|
-
# Last download time = most recent local file mtime (in s3-staging)
|
|
340
|
-
last_download = if Dir.exist?(staging_dir)
|
|
341
|
-
local_files = Dir.glob(File.join(staging_dir, '**/*')).select { |f| File.file?(f) }
|
|
342
|
-
local_files.map { |f| File.mtime(f) }.max if local_files.any?
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
{ last_upload: last_upload, last_download: last_download }
|
|
172
|
+
S3StatusChecker.new(brand, project_id, **delegated_opts).sync_timestamps
|
|
346
173
|
end
|
|
347
174
|
|
|
348
175
|
private
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appydave
|
|
4
|
+
module Tools
|
|
5
|
+
module Dam
|
|
6
|
+
# Handles S3 status and sync state operations.
|
|
7
|
+
# Inherits shared infrastructure and helpers from S3Base.
|
|
8
|
+
class S3StatusChecker < S3Base
|
|
9
|
+
# Show sync status
|
|
10
|
+
def status
|
|
11
|
+
project_dir = project_directory_path
|
|
12
|
+
staging_dir = File.join(project_dir, 's3-staging')
|
|
13
|
+
|
|
14
|
+
unless Dir.exist?(project_dir)
|
|
15
|
+
puts "❌ Project not found: #{brand}/#{project_id}"
|
|
16
|
+
puts ''
|
|
17
|
+
puts ' This project does not exist locally.'
|
|
18
|
+
puts ' Possible causes:'
|
|
19
|
+
puts ' - Project name might be misspelled'
|
|
20
|
+
puts ' - Project may not exist in this brand'
|
|
21
|
+
puts ''
|
|
22
|
+
puts " Try: dam list #{brand} # See all projects for this brand"
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
s3_files = list_s3_files
|
|
27
|
+
local_files = list_local_files(staging_dir)
|
|
28
|
+
|
|
29
|
+
s3_files_map = s3_files.each_with_object({}) do |file, hash|
|
|
30
|
+
relative_path = extract_relative_path(file['Key'])
|
|
31
|
+
hash[relative_path] = file
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if s3_files.empty? && local_files.empty?
|
|
35
|
+
puts "ℹ️ No files in S3 or s3-staging/ for #{brand}/#{project_id}"
|
|
36
|
+
puts ''
|
|
37
|
+
puts ' This project exists but has no heavy files ready for S3 sync.'
|
|
38
|
+
puts ''
|
|
39
|
+
puts ' Next steps:'
|
|
40
|
+
puts " 1. Add video files to: #{staging_dir}/"
|
|
41
|
+
puts " 2. Upload to S3: dam s3-up #{brand} #{project_id}"
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
puts "📊 S3 Sync Status for #{brand}/#{project_id}"
|
|
46
|
+
|
|
47
|
+
if s3_files.any?
|
|
48
|
+
most_recent = s3_files.map { |f| f['LastModified'] }.compact.max
|
|
49
|
+
if most_recent
|
|
50
|
+
time_ago = format_time_ago(Time.now - most_recent)
|
|
51
|
+
puts " Last synced: #{time_ago} ago (#{most_recent.strftime('%Y-%m-%d %H:%M')})"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
puts ''
|
|
55
|
+
|
|
56
|
+
all_paths = (s3_files_map.keys + local_files.keys).uniq.sort
|
|
57
|
+
|
|
58
|
+
total_s3_size = 0
|
|
59
|
+
total_local_size = 0
|
|
60
|
+
|
|
61
|
+
all_paths.each do |relative_path|
|
|
62
|
+
s3_file = s3_files_map[relative_path]
|
|
63
|
+
local_file = File.join(staging_dir, relative_path)
|
|
64
|
+
|
|
65
|
+
if s3_file && File.exist?(local_file)
|
|
66
|
+
s3_size = s3_file['Size']
|
|
67
|
+
local_size = File.size(local_file)
|
|
68
|
+
total_s3_size += s3_size
|
|
69
|
+
total_local_size += local_size
|
|
70
|
+
|
|
71
|
+
s3_etag = s3_file['ETag'].gsub('"', '')
|
|
72
|
+
match_status = compare_files(local_file: local_file, s3_etag: s3_etag, s3_size: s3_size)
|
|
73
|
+
|
|
74
|
+
if match_status == :synced
|
|
75
|
+
status_label = multipart_etag?(s3_etag) ? 'synced*' : 'synced'
|
|
76
|
+
puts " ✓ #{relative_path} (#{file_size_human(s3_size)}) [#{status_label}]"
|
|
77
|
+
else
|
|
78
|
+
puts " ⚠️ #{relative_path} (#{file_size_human(s3_size)}) [modified]"
|
|
79
|
+
end
|
|
80
|
+
elsif s3_file
|
|
81
|
+
s3_size = s3_file['Size']
|
|
82
|
+
total_s3_size += s3_size
|
|
83
|
+
puts " ☁️ #{relative_path} (#{file_size_human(s3_size)}) [S3 only]"
|
|
84
|
+
else
|
|
85
|
+
local_size = File.size(local_file)
|
|
86
|
+
total_local_size += local_size
|
|
87
|
+
puts " 📁 #{relative_path} (#{file_size_human(local_size)}) [local only]"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
puts ''
|
|
92
|
+
puts "S3 files: #{s3_files.size}, Local files: #{local_files.size}"
|
|
93
|
+
puts "S3 size: #{file_size_human(total_s3_size)}, Local size: #{file_size_human(total_local_size)}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Calculate 3-state S3 sync status
|
|
97
|
+
# @return [String] One of: '↑ upload', '↓ download', '✓ synced', 'none'
|
|
98
|
+
def calculate_sync_status
|
|
99
|
+
project_dir = project_directory_path
|
|
100
|
+
staging_dir = File.join(project_dir, 's3-staging')
|
|
101
|
+
|
|
102
|
+
return 'none' unless Dir.exist?(staging_dir)
|
|
103
|
+
|
|
104
|
+
begin
|
|
105
|
+
s3_files = list_s3_files
|
|
106
|
+
rescue StandardError
|
|
107
|
+
return 'none'
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
local_files = list_local_files(staging_dir)
|
|
111
|
+
|
|
112
|
+
return 'none' if s3_files.empty? && local_files.empty?
|
|
113
|
+
|
|
114
|
+
s3_files_map = s3_files.each_with_object({}) do |file, hash|
|
|
115
|
+
relative_path = extract_relative_path(file['Key'])
|
|
116
|
+
hash[relative_path] = file
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
needs_upload = false
|
|
120
|
+
needs_download = false
|
|
121
|
+
|
|
122
|
+
local_files.each_key do |relative_path|
|
|
123
|
+
local_file = File.join(staging_dir, relative_path)
|
|
124
|
+
s3_file = s3_files_map[relative_path]
|
|
125
|
+
|
|
126
|
+
if s3_file
|
|
127
|
+
s3_etag = s3_file['ETag'].gsub('"', '')
|
|
128
|
+
s3_size = s3_file['Size']
|
|
129
|
+
match_status = compare_files(local_file: local_file, s3_etag: s3_etag, s3_size: s3_size)
|
|
130
|
+
needs_upload = true if match_status != :synced
|
|
131
|
+
else
|
|
132
|
+
needs_upload = true
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
s3_files_map.each_key do |relative_path|
|
|
137
|
+
local_file = File.join(staging_dir, relative_path)
|
|
138
|
+
needs_download = true unless File.exist?(local_file)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if needs_upload && needs_download
|
|
142
|
+
'⚠️ both'
|
|
143
|
+
elsif needs_upload
|
|
144
|
+
'↑ upload'
|
|
145
|
+
elsif needs_download
|
|
146
|
+
'↓ download'
|
|
147
|
+
else
|
|
148
|
+
'✓ synced'
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Calculate S3 sync timestamps (last upload/download times)
|
|
153
|
+
# @return [Hash] { last_upload: Time|nil, last_download: Time|nil }
|
|
154
|
+
def sync_timestamps
|
|
155
|
+
project_dir = project_directory_path
|
|
156
|
+
staging_dir = File.join(project_dir, 's3-staging')
|
|
157
|
+
|
|
158
|
+
return { last_upload: nil, last_download: nil } unless Dir.exist?(staging_dir)
|
|
159
|
+
|
|
160
|
+
begin
|
|
161
|
+
s3_files = list_s3_files
|
|
162
|
+
rescue StandardError
|
|
163
|
+
return { last_upload: nil, last_download: nil }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
last_upload = s3_files.map { |f| f['LastModified'] }.compact.max if s3_files.any?
|
|
167
|
+
|
|
168
|
+
last_download = if Dir.exist?(staging_dir)
|
|
169
|
+
local_files = Dir.glob(File.join(staging_dir, '**/*')).select { |f| File.file?(f) }
|
|
170
|
+
local_files.map { |f| File.mtime(f) }.max if local_files.any?
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
{ last_upload: last_upload, last_download: last_download }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
data/lib/appydave/tools.rb
CHANGED
|
@@ -70,6 +70,7 @@ require 'appydave/tools/dam/config_loader'
|
|
|
70
70
|
require 'appydave/tools/dam/s3_base'
|
|
71
71
|
require 'appydave/tools/dam/s3_uploader'
|
|
72
72
|
require 'appydave/tools/dam/s3_downloader'
|
|
73
|
+
require 'appydave/tools/dam/s3_status_checker'
|
|
73
74
|
require 'appydave/tools/dam/s3_operations'
|
|
74
75
|
require 'appydave/tools/dam/s3_scanner'
|
|
75
76
|
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.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cruwys
|
|
@@ -379,6 +379,7 @@ files:
|
|
|
379
379
|
- lib/appydave/tools/dam/s3_operations.rb
|
|
380
380
|
- lib/appydave/tools/dam/s3_scan_command.rb
|
|
381
381
|
- lib/appydave/tools/dam/s3_scanner.rb
|
|
382
|
+
- lib/appydave/tools/dam/s3_status_checker.rb
|
|
382
383
|
- lib/appydave/tools/dam/s3_uploader.rb
|
|
383
384
|
- lib/appydave/tools/dam/share_operations.rb
|
|
384
385
|
- lib/appydave/tools/dam/ssd_status.rb
|