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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b37b062c3680912049eb80e1cb4c40a213429d80e4a80fd5a9ea687e68bda536
4
- data.tar.gz: a8ee199b0664434908e0178cc21956620a87455540f459ee6e1f85560cbe02f8
3
+ metadata.gz: a66f70a8ea85fce726ea3da189166f623c7fb1ca007f70b1b5fb75f3fd828077
4
+ data.tar.gz: 423a8d4309fb166084940efe6bcb7fbd3ab9d620094b006bf95194aef671719a
5
5
  SHA512:
6
- metadata.gz: 1f7e6164d4e947be359dda0e27b61507b35bbe65e10073760e765b12ea755bfcb66d38784567e1404fe8474745dc34782fe61684c322e091697b4f7df94fccab
7
- data.tar.gz: f12c43fe505fd799f991c6f158bc30a82df78b3676aceb6a77208d24bc443293f3e5ee32172fe18f70034d03f9ab6d4c0f5dbee6319e29e62569a6ac7847e4e2
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
- project_dir = project_directory_path
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
- project_dir = project_directory_path
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
- project_dir = project_directory_path
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Appydave
4
4
  module Tools
5
- VERSION = '0.77.4'
5
+ VERSION = '0.77.5'
6
6
  end
7
7
  end
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appydave-tools",
3
- "version": "0.77.4",
3
+ "version": "0.77.5",
4
4
  "description": "AppyDave YouTube Automation Tools",
5
5
  "scripts": {
6
6
  "release": "semantic-release"
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
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