appydave-tools 0.38.0 → 0.40.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/CHANGELOG.md +21 -0
- data/lib/appydave/tools/configuration/config.rb +93 -1
- data/lib/appydave/tools/dam/brand_resolver.rb +118 -0
- data/lib/appydave/tools/dam/config.rb +2 -25
- data/lib/appydave/tools/dam/project_resolver.rb +2 -2
- data/lib/appydave/tools/dam/s3_operations.rb +62 -8
- 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: 9610c23fa022740f9082e2bd0086bf7727cfe96437309b099a1bc5fbe55f58ea
|
|
4
|
+
data.tar.gz: d556ee4ced131cfc9f7b8244edf9c2c464d5d3692a669cb0f7102831fccf1689
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f337f930defe037888a8ebc8dd19f6562519b39170b33a8944e68cc050059a65902d41c788d6cafa2c6132b2d2ee6f408ad54a420ce68c1bb74fb392ab9dc716
|
|
7
|
+
data.tar.gz: f7f0933afa44c8e3a79aa3ceb6b53db950d0acb9e3b5ba17daf3acc46eafc97afd19071c0e335485d3345c08c9218c2285f5dcc0f74332bd87a3bf136a7dc210
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
# [0.39.0](https://github.com/appydave/appydave-tools/compare/v0.38.0...v0.39.0) (2025-11-21)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* create BrandResolver class to centralize brand name resolution and eliminate ~30 lines of duplication from Config.expand_brand, fixing case-sensitivity bugs ([d6cce4b](https://github.com/appydave/appydave-tools/commit/d6cce4b0c1df6d7efcd40db52e80534c4014fdb5))
|
|
7
|
+
|
|
8
|
+
# [0.38.0](https://github.com/appydave/appydave-tools/compare/v0.37.0...v0.38.0) (2025-11-21)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* rename FileUtils to FileHelper to avoid Ruby stdlib conflict ([e441b43](https://github.com/appydave/appydave-tools/commit/e441b4356ec40a66f5562e9cf0ec74f562b449b2))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* add DAM exception hierarchy for consistent error handling ([c885590](https://github.com/appydave/appydave-tools/commit/c885590be721d2b2803fd6aaa7001afc8cdbd285))
|
|
19
|
+
* extract FileUtils module to eliminate 40 lines of duplication ([643748e](https://github.com/appydave/appydave-tools/commit/643748ea583eba3605e1a6ce17e117db18f66c4c))
|
|
20
|
+
* extract GitHelper module to eliminate ~90 lines of git operations duplication across status.rb, repo_status.rb, and repo_push.rb ([f32dc1d](https://github.com/appydave/appydave-tools/commit/f32dc1d54877a9a981878a6e7c203922e50004fe))
|
|
21
|
+
|
|
1
22
|
# [0.37.0](https://github.com/appydave/appydave-tools/compare/v0.36.0...v0.37.0) (2025-11-21)
|
|
2
23
|
|
|
3
24
|
|
|
@@ -3,7 +3,33 @@
|
|
|
3
3
|
module Appydave
|
|
4
4
|
module Tools
|
|
5
5
|
module Configuration
|
|
6
|
-
#
|
|
6
|
+
# Central configuration management for appydave-tools
|
|
7
|
+
#
|
|
8
|
+
# Thread-safe singleton pattern with memoization for registered configurations.
|
|
9
|
+
# Calling `Config.configure` multiple times is safe and idempotent.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# Config.configure # Load default configuration (idempotent)
|
|
13
|
+
# Config.settings.video_projects_root # Access settings
|
|
14
|
+
# Config.brands.get_brand('appydave') # Access brands
|
|
15
|
+
#
|
|
16
|
+
# @example DAM module usage pattern
|
|
17
|
+
# # Config.configure called once at module load time
|
|
18
|
+
# # All subsequent calls within DAM classes are no-ops (memoized)
|
|
19
|
+
# def some_method
|
|
20
|
+
# Config.configure # Safe to call - returns immediately if already configured
|
|
21
|
+
# brand = Config.brands.get_brand('appydave')
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# @example Registered configurations
|
|
25
|
+
# Config.settings # => SettingsConfig instance
|
|
26
|
+
# Config.brands # => BrandsConfig instance
|
|
27
|
+
# Config.channels # => ChannelsConfig instance
|
|
28
|
+
# Config.youtube_automation # => YoutubeAutomationConfig instance
|
|
29
|
+
#
|
|
30
|
+
# @note Configuration instances are created once on first registration and reused
|
|
31
|
+
# for all subsequent accesses. This prevents unnecessary file I/O and ensures
|
|
32
|
+
# consistent state across the application.
|
|
7
33
|
class Config
|
|
8
34
|
class << self
|
|
9
35
|
include KLog::Logging
|
|
@@ -12,6 +38,25 @@ module Appydave
|
|
|
12
38
|
attr_reader :configurations
|
|
13
39
|
attr_reader :default_block
|
|
14
40
|
|
|
41
|
+
# Load configuration using either provided block or default configuration
|
|
42
|
+
#
|
|
43
|
+
# This method is idempotent and thread-safe. Calling it multiple times
|
|
44
|
+
# has no negative side effects - configurations are memoized on first call.
|
|
45
|
+
#
|
|
46
|
+
# @yield [Config] configuration object for manual setup
|
|
47
|
+
# @return [void]
|
|
48
|
+
# @raise [Error] if no block provided and no default_block set
|
|
49
|
+
#
|
|
50
|
+
# @example With block (manual configuration)
|
|
51
|
+
# Config.configure do |config|
|
|
52
|
+
# config.config_path = '/custom/path'
|
|
53
|
+
# config.register(:settings, SettingsConfig)
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
# @example Without block (uses default_block)
|
|
57
|
+
# Config.set_default { |config| config.register(:settings, SettingsConfig) }
|
|
58
|
+
# Config.configure # Uses default_block
|
|
59
|
+
# Config.configure # Safe to call again - no-op due to memoization
|
|
15
60
|
def configure
|
|
16
61
|
if block_given?
|
|
17
62
|
yield self
|
|
@@ -23,6 +68,23 @@ module Appydave
|
|
|
23
68
|
ensure_config_directory
|
|
24
69
|
end
|
|
25
70
|
|
|
71
|
+
# Register a configuration class with memoization
|
|
72
|
+
#
|
|
73
|
+
# Creates a single instance of the configuration class on first call.
|
|
74
|
+
# Subsequent calls return the same instance (memoized). This prevents
|
|
75
|
+
# unnecessary file I/O and ensures consistent configuration state.
|
|
76
|
+
#
|
|
77
|
+
# @param key [Symbol] configuration identifier (e.g., :settings, :brands)
|
|
78
|
+
# @param klass [Class] configuration class to instantiate
|
|
79
|
+
# @return [Object] configuration instance
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# Config.register(:settings, SettingsConfig)
|
|
83
|
+
# Config.settings # => SettingsConfig instance (created on first access)
|
|
84
|
+
# Config.settings # => Same instance (memoized)
|
|
85
|
+
#
|
|
86
|
+
# @note This method implements lazy initialization - the configuration
|
|
87
|
+
# instance is only created when first accessed, not at registration time.
|
|
26
88
|
def register(key, klass)
|
|
27
89
|
@configurations ||= {}
|
|
28
90
|
# Only create new instance if not already registered (prevents multiple reloads)
|
|
@@ -30,10 +92,26 @@ module Appydave
|
|
|
30
92
|
end
|
|
31
93
|
|
|
32
94
|
# Reset all configurations (primarily for testing)
|
|
95
|
+
#
|
|
96
|
+
# Clears all memoized configuration instances. Use this in test teardown
|
|
97
|
+
# to ensure each test starts with a clean configuration state.
|
|
98
|
+
#
|
|
99
|
+
# @return [void]
|
|
100
|
+
#
|
|
101
|
+
# @example RSpec usage
|
|
102
|
+
# after { Config.reset }
|
|
33
103
|
def reset
|
|
34
104
|
@configurations = nil
|
|
35
105
|
end
|
|
36
106
|
|
|
107
|
+
# Dynamic accessor for registered configurations
|
|
108
|
+
#
|
|
109
|
+
# Provides method-style access to registered configuration instances.
|
|
110
|
+
# Called when accessing Config.settings, Config.brands, etc.
|
|
111
|
+
#
|
|
112
|
+
# @param method_name [Symbol] configuration key
|
|
113
|
+
# @return [Object] configuration instance
|
|
114
|
+
# @raise [Error] if configurations not registered or key not found
|
|
37
115
|
def method_missing(method_name, *_args)
|
|
38
116
|
raise Appydave::Tools::Error, 'Configuration has never been registered' if @configurations.nil?
|
|
39
117
|
raise Appydave::Tools::Error, "Configuration not available: #{method_name}" unless @configurations.key?(method_name)
|
|
@@ -45,18 +123,27 @@ module Appydave
|
|
|
45
123
|
@configurations.key?(method_name) || super
|
|
46
124
|
end
|
|
47
125
|
|
|
126
|
+
# Save all registered configurations to their respective files
|
|
127
|
+
# @return [void]
|
|
48
128
|
def save
|
|
49
129
|
configurations.each_value(&:save)
|
|
50
130
|
end
|
|
51
131
|
|
|
132
|
+
# Set default configuration block used when configure called without block
|
|
133
|
+
# @yield [Config] configuration block to execute by default
|
|
134
|
+
# @return [Proc] the stored block
|
|
52
135
|
def set_default(&block)
|
|
53
136
|
@default_block = block
|
|
54
137
|
end
|
|
55
138
|
|
|
139
|
+
# Load all registered configurations from their respective files
|
|
140
|
+
# @return [void]
|
|
56
141
|
def load
|
|
57
142
|
configurations.each_value(&:load)
|
|
58
143
|
end
|
|
59
144
|
|
|
145
|
+
# Open configuration directory in VS Code
|
|
146
|
+
# @return [void]
|
|
60
147
|
def edit
|
|
61
148
|
ensure_config_directory
|
|
62
149
|
puts "Edit configuration: #{config_path}"
|
|
@@ -64,6 +151,8 @@ module Appydave
|
|
|
64
151
|
Open3.capture3(open_vscode)
|
|
65
152
|
end
|
|
66
153
|
|
|
154
|
+
# Debug output for all configurations
|
|
155
|
+
# @return [void]
|
|
67
156
|
def debug
|
|
68
157
|
log.kv 'Configuration Path', config_path
|
|
69
158
|
configurations.each_value(&:debug)
|
|
@@ -74,6 +163,9 @@ module Appydave
|
|
|
74
163
|
# configurations.each_value(&:print)
|
|
75
164
|
# end
|
|
76
165
|
|
|
166
|
+
# Print specific configurations or all if no keys provided
|
|
167
|
+
# @param keys [Array<String, Symbol>] configuration keys to print
|
|
168
|
+
# @return [void]
|
|
77
169
|
def print(*keys)
|
|
78
170
|
if keys.empty?
|
|
79
171
|
keys = configurations.keys
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appydave
|
|
4
|
+
module Tools
|
|
5
|
+
module Dam
|
|
6
|
+
# Centralized brand name resolution and transformation
|
|
7
|
+
#
|
|
8
|
+
# Handles conversion between:
|
|
9
|
+
# - Shortcuts: 'appydave', 'ad', 'joy', 'ss'
|
|
10
|
+
# - Config keys: 'appydave', 'beauty-and-joy', 'supportsignal'
|
|
11
|
+
# - Display names: 'v-appydave', 'v-beauty-and-joy', 'v-supportsignal'
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# BrandResolver.expand('ad') # => 'v-appydave'
|
|
15
|
+
# BrandResolver.normalize('v-voz') # => 'voz'
|
|
16
|
+
# BrandResolver.to_config_key('ad') # => 'appydave'
|
|
17
|
+
# BrandResolver.to_display('voz') # => 'v-voz'
|
|
18
|
+
class BrandResolver
|
|
19
|
+
class << self
|
|
20
|
+
# Expand shortcut or key to full display name
|
|
21
|
+
# @param shortcut [String] Brand shortcut or key
|
|
22
|
+
# @return [String] Full brand name with v- prefix
|
|
23
|
+
def expand(shortcut)
|
|
24
|
+
return shortcut.to_s if shortcut.to_s.start_with?('v-')
|
|
25
|
+
|
|
26
|
+
key = to_config_key(shortcut)
|
|
27
|
+
"v-#{key}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Normalize brand name to config key (strip v- prefix)
|
|
31
|
+
# @param brand [String] Brand name (with or without v-)
|
|
32
|
+
# @return [String] Config key without v- prefix
|
|
33
|
+
def normalize(brand)
|
|
34
|
+
brand.to_s.sub(/^v-/, '')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Convert to config key (handles shortcuts)
|
|
38
|
+
# @param input [String] Shortcut, key, or display name
|
|
39
|
+
# @return [String] Config key
|
|
40
|
+
def to_config_key(input)
|
|
41
|
+
# Strip v- prefix first
|
|
42
|
+
normalized = normalize(input)
|
|
43
|
+
|
|
44
|
+
# Look up from brands.json
|
|
45
|
+
Appydave::Tools::Configuration::Config.configure
|
|
46
|
+
brands_config = Appydave::Tools::Configuration::Config.brands
|
|
47
|
+
|
|
48
|
+
# Check if matches brand key (case-insensitive)
|
|
49
|
+
brand = brands_config.brands.find { |b| b.key.downcase == normalized.downcase }
|
|
50
|
+
return brand.key if brand
|
|
51
|
+
|
|
52
|
+
# Check if matches shortcut (case-insensitive)
|
|
53
|
+
brand = brands_config.brands.find { |b| b.shortcut.downcase == normalized.downcase }
|
|
54
|
+
return brand.key if brand
|
|
55
|
+
|
|
56
|
+
# Fall back to hardcoded shortcuts (backward compatibility)
|
|
57
|
+
case normalized.downcase
|
|
58
|
+
when 'ad' then 'appydave'
|
|
59
|
+
when 'joy' then 'beauty-and-joy'
|
|
60
|
+
when 'ss' then 'supportsignal'
|
|
61
|
+
else
|
|
62
|
+
normalized.downcase
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Convert to display name (always v- prefix)
|
|
67
|
+
# @param input [String] Shortcut, key, or display name
|
|
68
|
+
# @return [String] Display name with v- prefix
|
|
69
|
+
def to_display(input)
|
|
70
|
+
expand(input)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Validate brand exists in filesystem
|
|
74
|
+
# @param brand [String] Brand to validate
|
|
75
|
+
# @raise [BrandNotFoundError] if brand invalid
|
|
76
|
+
# @return [String] Config key if valid
|
|
77
|
+
def validate(brand)
|
|
78
|
+
key = to_config_key(brand)
|
|
79
|
+
|
|
80
|
+
# Build brand path (avoiding circular dependency with Config.brand_path)
|
|
81
|
+
Appydave::Tools::Configuration::Config.configure
|
|
82
|
+
brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(key)
|
|
83
|
+
|
|
84
|
+
# If brand has configured video_projects path, use it
|
|
85
|
+
if brand_info.locations.video_projects && !brand_info.locations.video_projects.empty?
|
|
86
|
+
brand_path = brand_info.locations.video_projects
|
|
87
|
+
else
|
|
88
|
+
# Fall back to projects_root + expanded brand name
|
|
89
|
+
root = Config.projects_root
|
|
90
|
+
brand_path = File.join(root, expand(key))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
unless Dir.exist?(brand_path)
|
|
94
|
+
available = Config.available_brands_display
|
|
95
|
+
raise BrandNotFoundError.new(brand, available)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
key
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
raise BrandNotFoundError, e.message unless e.is_a?(BrandNotFoundError)
|
|
101
|
+
|
|
102
|
+
raise
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if brand exists (returns boolean instead of raising)
|
|
106
|
+
# @param brand [String] Brand to check
|
|
107
|
+
# @return [Boolean] true if brand exists
|
|
108
|
+
def exists?(brand)
|
|
109
|
+
validate(brand)
|
|
110
|
+
true
|
|
111
|
+
rescue BrandNotFoundError
|
|
112
|
+
false
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -85,34 +85,11 @@ module Appydave
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
# Expand brand shortcut to full brand name
|
|
88
|
-
#
|
|
88
|
+
# Delegates to BrandResolver for centralized brand resolution
|
|
89
89
|
# @param shortcut [String] Brand shortcut (e.g., 'appydave', 'ad', 'APPYDAVE')
|
|
90
90
|
# @return [String] Full brand name (e.g., 'v-appydave')
|
|
91
91
|
def expand_brand(shortcut)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return shortcut_str if shortcut_str.start_with?('v-')
|
|
95
|
-
|
|
96
|
-
# Try to read from brands.json
|
|
97
|
-
Appydave::Tools::Configuration::Config.configure
|
|
98
|
-
brands_config = Appydave::Tools::Configuration::Config.brands
|
|
99
|
-
|
|
100
|
-
# Check if input matches a brand key (case-insensitive)
|
|
101
|
-
brand = brands_config.brands.find { |b| b.key.downcase == shortcut_str.downcase }
|
|
102
|
-
return "v-#{brand.key}" if brand
|
|
103
|
-
|
|
104
|
-
# Check if input matches a brand shortcut (case-insensitive)
|
|
105
|
-
brand = brands_config.brands.find { |b| b.shortcut.downcase == shortcut_str.downcase }
|
|
106
|
-
return "v-#{brand.key}" if brand
|
|
107
|
-
|
|
108
|
-
# Fall back to hardcoded shortcuts for backwards compatibility
|
|
109
|
-
normalized = shortcut_str.downcase
|
|
110
|
-
case normalized
|
|
111
|
-
when 'joy' then 'v-beauty-and-joy'
|
|
112
|
-
when 'ss' then 'v-supportsignal'
|
|
113
|
-
else
|
|
114
|
-
"v-#{normalized}"
|
|
115
|
-
end
|
|
92
|
+
BrandResolver.expand(shortcut)
|
|
116
93
|
end
|
|
117
94
|
|
|
118
95
|
# Get list of available brands
|
|
@@ -116,9 +116,9 @@ module Appydave
|
|
|
116
116
|
# Check if we're inside a v-* directory
|
|
117
117
|
if current =~ %r{/(v-[^/]+)/([^/]+)/?}
|
|
118
118
|
brand_with_prefix = ::Regexp.last_match(1)
|
|
119
|
-
project = ::Regexp.last_match(2) # Capture BEFORE
|
|
119
|
+
project = ::Regexp.last_match(2) # Capture BEFORE normalize() which resets Regexp.last_match
|
|
120
120
|
# Strip 'v-' prefix to get brand key (e.g., 'v-supportsignal' → 'supportsignal')
|
|
121
|
-
brand_key =
|
|
121
|
+
brand_key = BrandResolver.normalize(brand_with_prefix)
|
|
122
122
|
return [brand_key, project] if project_exists?(brand_key, project)
|
|
123
123
|
end
|
|
124
124
|
|
|
@@ -140,6 +140,7 @@ module Appydave
|
|
|
140
140
|
skipped = 0
|
|
141
141
|
failed = 0
|
|
142
142
|
|
|
143
|
+
# rubocop:disable Metrics/BlockLength
|
|
143
144
|
files.each do |file|
|
|
144
145
|
relative_path = file.sub("#{staging_dir}/", '')
|
|
145
146
|
|
|
@@ -158,12 +159,31 @@ module Appydave
|
|
|
158
159
|
if local_md5 == s3_md5
|
|
159
160
|
puts " ⏭️ Skipped: #{relative_path} (unchanged)"
|
|
160
161
|
skipped += 1
|
|
161
|
-
elsif upload_file(file, s3_path, dry_run: dry_run)
|
|
162
|
-
uploaded += 1
|
|
163
162
|
else
|
|
164
|
-
|
|
163
|
+
# Warn if we're about to overwrite an existing S3 file
|
|
164
|
+
if s3_md5 && s3_md5 != local_md5
|
|
165
|
+
puts " ⚠️ Warning: #{relative_path} exists in S3 with different content"
|
|
166
|
+
|
|
167
|
+
# Try to get S3 timestamp for comparison
|
|
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...'
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
if upload_file(file, s3_path, dry_run: dry_run)
|
|
180
|
+
uploaded += 1
|
|
181
|
+
else
|
|
182
|
+
failed += 1
|
|
183
|
+
end
|
|
165
184
|
end
|
|
166
185
|
end
|
|
186
|
+
# rubocop:enable Metrics/BlockLength
|
|
167
187
|
|
|
168
188
|
puts ''
|
|
169
189
|
puts '✅ Upload complete!'
|
|
@@ -207,13 +227,29 @@ module Appydave
|
|
|
207
227
|
if local_md5 == s3_md5
|
|
208
228
|
puts " ⏭️ Skipped: #{relative_path} (unchanged)"
|
|
209
229
|
skipped += 1
|
|
210
|
-
elsif download_file(key, local_file, dry_run: dry_run)
|
|
211
|
-
downloaded += 1
|
|
212
230
|
else
|
|
213
|
-
|
|
231
|
+
# Warn if we're about to overwrite an existing local file
|
|
232
|
+
if local_md5 && local_md5 != s3_md5
|
|
233
|
+
puts " ⚠️ Warning: #{relative_path} exists locally with different content"
|
|
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...'
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
if download_file(key, local_file, dry_run: dry_run)
|
|
247
|
+
downloaded += 1
|
|
248
|
+
else
|
|
249
|
+
failed += 1
|
|
250
|
+
end
|
|
214
251
|
end
|
|
215
252
|
end
|
|
216
|
-
|
|
217
253
|
puts ''
|
|
218
254
|
puts '✅ Download complete!'
|
|
219
255
|
puts " Downloaded: #{downloaded}, Skipped: #{skipped}, Failed: #{failed}"
|
|
@@ -634,13 +670,31 @@ module Appydave
|
|
|
634
670
|
{
|
|
635
671
|
'Key' => obj.key,
|
|
636
672
|
'Size' => obj.size,
|
|
637
|
-
'ETag' => obj.etag
|
|
673
|
+
'ETag' => obj.etag,
|
|
674
|
+
'LastModified' => obj.last_modified
|
|
638
675
|
}
|
|
639
676
|
end
|
|
640
677
|
rescue Aws::S3::Errors::ServiceError
|
|
641
678
|
[]
|
|
642
679
|
end
|
|
643
680
|
|
|
681
|
+
# Get full S3 file info including timestamp
|
|
682
|
+
def get_s3_file_info(s3_key)
|
|
683
|
+
response = s3_client.head_object(
|
|
684
|
+
bucket: brand_info.aws.s3_bucket,
|
|
685
|
+
key: s3_key
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
{
|
|
689
|
+
'Key' => s3_key,
|
|
690
|
+
'Size' => response.content_length,
|
|
691
|
+
'ETag' => response.etag,
|
|
692
|
+
'LastModified' => response.last_modified
|
|
693
|
+
}
|
|
694
|
+
rescue Aws::S3::Errors::NotFound, Aws::S3::Errors::ServiceError
|
|
695
|
+
nil
|
|
696
|
+
end
|
|
697
|
+
|
|
644
698
|
# List local files in staging directory
|
|
645
699
|
def list_local_files(staging_dir)
|
|
646
700
|
return {} unless Dir.exist?(staging_dir)
|
data/lib/appydave/tools.rb
CHANGED
|
@@ -55,6 +55,7 @@ require 'appydave/tools/subtitle_processor/join'
|
|
|
55
55
|
require 'appydave/tools/dam/errors'
|
|
56
56
|
require 'appydave/tools/dam/file_helper'
|
|
57
57
|
require 'appydave/tools/dam/git_helper'
|
|
58
|
+
require 'appydave/tools/dam/brand_resolver'
|
|
58
59
|
require 'appydave/tools/dam/config'
|
|
59
60
|
require 'appydave/tools/dam/project_resolver'
|
|
60
61
|
require 'appydave/tools/dam/config_loader'
|
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.
|
|
4
|
+
version: 0.40.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cruwys
|
|
@@ -297,6 +297,7 @@ files:
|
|
|
297
297
|
- lib/appydave/tools/configuration/models/settings_config.rb
|
|
298
298
|
- lib/appydave/tools/configuration/models/youtube_automation_config.rb
|
|
299
299
|
- lib/appydave/tools/configuration/openai.rb
|
|
300
|
+
- lib/appydave/tools/dam/brand_resolver.rb
|
|
300
301
|
- lib/appydave/tools/dam/config.rb
|
|
301
302
|
- lib/appydave/tools/dam/config_loader.rb
|
|
302
303
|
- lib/appydave/tools/dam/errors.rb
|