kdeploy 1.2.27 → 1.2.28

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: d68017d3cc4506fb9d54391bdc5aa9bca2be0128c05d5e723e2de5add79f193b
4
- data.tar.gz: d53007ff89ed7c0f1251dad46144943cefbd87e8300194fe8c9ed3af5c795dc6
3
+ metadata.gz: 6e6d5731024f6c6038b668226945d6360dcd2e6abab1e68a54872b0e815bacd0
4
+ data.tar.gz: 9c8505220dfb4999a9fa62a8607428b880b7bd954c7ecc9707b6dd5a72c65e25
5
5
  SHA512:
6
- metadata.gz: 1fafc72a06a49340e5c39adea988f5ace68fb07a7ba74729fe96cfac4b1ed05fc4270c1938c2bc952eae7283137c5bc79fee0f3c61f0b024cba1d24eccb201bc
7
- data.tar.gz: d02a0a6dfd913f06ced94cd4b018082be3aedc381d08197605bea0413811873baf662e9a3b0b4c1b145192e66008be949cc1f96d10b11e6fd0f26a00b9cfc33d
6
+ metadata.gz: aa53413eb0414244474a7f1e838074ecdefa34ae7e146a7711abb04317babdeb1ae6b1492a5f33c2357fb311ab58657f19e4155eace3ad045e338f2b3891938b
7
+ data.tar.gz: 26d4cd0add001ff1877f8fdc013f6ee89b9eb0d1fe6951cc197d202167613c9f69e85d8af10512cce08d7c76cdec1de5b208e2f595b4bb15a7a9754ecd680e4d
data/README.md CHANGED
@@ -44,6 +44,7 @@
44
44
  - 📝 **优雅的 Ruby DSL**: 简单而富有表现力的任务定义语法
45
45
  - 🚀 **并发执行**: 跨多个主机的高效并行任务处理
46
46
  - 📤 **文件上传支持**: 通过 SCP 轻松部署文件和模板
47
+ - 📁 **目录同步功能**: 递归同步目录,支持文件过滤和删除多余文件
47
48
  - 📊 **任务状态跟踪**: 实时执行监控,提供详细输出
48
49
  - 🔄 **ERB 模板支持**: 支持变量替换的动态配置生成
49
50
  - 🎯 **基于角色的部署**: 针对特定服务器角色进行有组织的部署
@@ -472,6 +473,48 @@ upload_template "./config/nginx.conf.erb", "/etc/nginx/nginx.conf",
472
473
  - `destination`: 远程文件路径
473
474
  - `variables`: 用于模板渲染的变量哈希
474
475
 
476
+ #### `sync` - 同步目录
477
+
478
+ 递归同步本地目录到远程服务器,支持文件过滤和删除多余文件。
479
+
480
+ ```ruby
481
+ # 基本同步
482
+ sync "./app", "/var/www/app"
483
+
484
+ # 同步并忽略特定文件/目录
485
+ sync "./app", "/var/www/app",
486
+ ignore: [".git", "*.log", "node_modules", "*.tmp"]
487
+
488
+ # 同步并删除远程多余文件
489
+ sync "./app", "/var/www/app",
490
+ ignore: [".git", "*.log"],
491
+ delete: true
492
+
493
+ # 排除特定文件(与 ignore 相同,但语义更清晰)
494
+ sync "./config", "/etc/app",
495
+ exclude: ["*.example", "*.bak", ".env.local"]
496
+ ```
497
+
498
+ **参数:**
499
+ - `source`: 本地源目录路径
500
+ - `destination`: 远程目标目录路径
501
+ - `ignore`: 要忽略的文件/目录模式数组(支持 .gitignore 风格的通配符)
502
+ - `exclude`: 与 `ignore` 相同,用于语义清晰
503
+ - `delete`: 布尔值,是否删除远程目录中不存在于源目录的文件(默认: false)
504
+
505
+ **忽略模式支持:**
506
+ - `*.log` - 匹配所有 .log 文件
507
+ - `node_modules` - 匹配 node_modules 目录或文件
508
+ - `**/*.tmp` - 递归匹配所有 .tmp 文件
509
+ - `.git` - 匹配 .git 目录
510
+ - `config/*.local` - 匹配 config 目录下的所有 .local 文件
511
+
512
+ **使用场景:**
513
+ - 部署应用程序代码
514
+ - 同步配置文件目录
515
+ - 同步静态资源文件
516
+ - 保持本地和远程目录结构一致
517
+
475
518
  ### 模板支持
476
519
 
477
520
  Kdeploy 支持 ERB(嵌入式 Ruby)模板,用于动态配置生成。
@@ -1005,6 +1048,23 @@ task :update_config, roles: :web do
1005
1048
  end
1006
1049
  ```
1007
1050
 
1051
+ #### 目录同步部署
1052
+
1053
+ ```ruby
1054
+ task :deploy_app, roles: :web do
1055
+ # 同步应用程序代码,忽略开发文件
1056
+ sync "./app", "/var/www/app",
1057
+ ignore: [".git", "*.log", "node_modules", ".env.local", "*.tmp"],
1058
+ delete: true
1059
+
1060
+ # 同步配置文件
1061
+ sync "./config", "/etc/app",
1062
+ exclude: ["*.example", "*.bak"]
1063
+
1064
+ run "sudo systemctl restart app"
1065
+ end
1066
+ ```
1067
+
1008
1068
  ## 📝 许可证
1009
1069
 
1010
1070
  该 gem 在 [MIT 许可证](https://opensource.org/licenses/MIT) 条款下作为开源提供。
data/lib/kdeploy/cli.rb CHANGED
@@ -162,6 +162,8 @@ module Kdeploy
162
162
  formatter.format_upload_steps(steps, shown)
163
163
  when :upload_template
164
164
  formatter.format_template_steps(steps, shown)
165
+ when :sync
166
+ formatter.format_sync_steps(steps, shown)
165
167
  when :run
166
168
  formatter.format_run_steps(steps, shown)
167
169
  else
@@ -53,6 +53,38 @@ module Kdeploy
53
53
  }
54
54
  end
55
55
 
56
+ def execute_sync(command, host_name)
57
+ source = command[:source]
58
+ destination = command[:destination]
59
+ ignore = command[:ignore] || []
60
+ exclude = command[:exclude] || []
61
+ delete = command[:delete] || false
62
+
63
+ description = "sync: #{source} -> #{destination}"
64
+ description += " (delete: #{delete})" if delete
65
+ show_command_header(host_name, :sync, description)
66
+
67
+ result, duration = measure_time do
68
+ @executor.sync_directory(
69
+ source,
70
+ destination,
71
+ ignore: ignore,
72
+ exclude: exclude,
73
+ delete: delete
74
+ )
75
+ end
76
+
77
+ {
78
+ command: "sync: #{source} -> #{destination}",
79
+ duration: duration,
80
+ type: :sync,
81
+ result: result,
82
+ uploaded: result[:uploaded],
83
+ deleted: result[:deleted],
84
+ total: result[:total]
85
+ }
86
+ end
87
+
56
88
  private
57
89
 
58
90
  def measure_time
@@ -13,6 +13,8 @@ module Kdeploy
13
13
  case cmd[:type]
14
14
  when :upload, :upload_template
15
15
  "#{cmd[:type]}_#{cmd[:source]}"
16
+ when :sync
17
+ "#{cmd[:type]}_#{cmd[:source]}"
16
18
  when :run
17
19
  "#{cmd[:type]}_#{cmd[:command].to_s.lines.first.strip}"
18
20
  else
@@ -26,6 +28,8 @@ module Kdeploy
26
28
  "upload #{command[:source]}"
27
29
  when :upload_template
28
30
  "template #{command[:source]}"
31
+ when :sync
32
+ "sync #{command[:source]}"
29
33
  when :run
30
34
  command[:command].to_s.lines.first.strip
31
35
  else
data/lib/kdeploy/dsl.rb CHANGED
@@ -121,6 +121,18 @@ module Kdeploy
121
121
  }
122
122
  end
123
123
 
124
+ def sync(source, destination, ignore: [], delete: false, exclude: [])
125
+ @kdeploy_commands ||= []
126
+ @kdeploy_commands << {
127
+ type: :sync,
128
+ source: source,
129
+ destination: destination,
130
+ ignore: Array(ignore),
131
+ exclude: Array(exclude),
132
+ delete: delete
133
+ }
134
+ end
135
+
124
136
  def inventory(&block)
125
137
  instance_eval(&block) if block_given?
126
138
  end
@@ -3,6 +3,9 @@
3
3
  require 'net/ssh'
4
4
  require 'net/scp'
5
5
  require 'pathname'
6
+ require 'find'
7
+ require 'shellwords'
8
+ require_relative 'file_filter'
6
9
 
7
10
  module Kdeploy
8
11
  # SSH/SCP executor for remote command execution and file operations
@@ -91,6 +94,51 @@ module Kdeploy
91
94
  raise TemplateError.new("Template upload failed: #{e.message}", e)
92
95
  end
93
96
 
97
+ def sync_directory(source, destination, ignore: [], exclude: [], delete: false, use_sudo: nil)
98
+ use_sudo = @use_sudo if use_sudo.nil?
99
+
100
+ # Resolve relative paths relative to base_dir
101
+ resolved_source = resolve_path(source)
102
+
103
+ # Validate source directory
104
+ raise FileNotFoundError, "Source directory not found: #{resolved_source}" unless File.directory?(resolved_source)
105
+
106
+ # Create file filter
107
+ all_patterns = ignore + exclude
108
+ filter = FileFilter.new(ignore_patterns: all_patterns)
109
+
110
+ # Collect files to sync
111
+ files_to_sync = collect_files_to_sync(resolved_source, filter)
112
+
113
+ # Upload files
114
+ uploaded_count = 0
115
+ source_path = Pathname.new(resolved_source)
116
+ files_to_sync.each do |file_path|
117
+ relative_path = Pathname.new(file_path).relative_path_from(source_path).to_s
118
+ remote_path = File.join(destination, relative_path).gsub(%r{/+}, '/')
119
+
120
+ # Ensure remote directory exists
121
+ remote_dir = File.dirname(remote_path)
122
+ ensure_remote_directory(remote_dir, use_sudo: use_sudo)
123
+
124
+ # Upload file
125
+ upload(file_path, remote_path, use_sudo: use_sudo)
126
+ uploaded_count += 1
127
+ end
128
+
129
+ # Delete extra files if requested
130
+ deleted_count = 0
131
+ deleted_count = delete_extra_files(resolved_source, destination, filter, use_sudo: use_sudo) if delete
132
+
133
+ {
134
+ uploaded: uploaded_count,
135
+ deleted: deleted_count,
136
+ total: files_to_sync.size
137
+ }
138
+ rescue StandardError => e
139
+ raise SCPError.new("Directory sync failed: #{e.message}", e)
140
+ end
141
+
94
142
  private
95
143
 
96
144
  def upload_with_sudo(source, destination)
@@ -184,5 +232,67 @@ module Kdeploy
184
232
  "sudo #{command}"
185
233
  end
186
234
  end
235
+
236
+ def collect_files_to_sync(source_dir, filter)
237
+ files = []
238
+ source_path = Pathname.new(source_dir)
239
+
240
+ Find.find(source_dir) do |file_path|
241
+ next if File.directory?(file_path)
242
+
243
+ relative_path = Pathname.new(file_path).relative_path_from(source_path).to_s
244
+ next if filter.ignored?(relative_path, source_dir)
245
+
246
+ files << file_path
247
+ end
248
+
249
+ files
250
+ end
251
+
252
+ def ensure_remote_directory(remote_dir, use_sudo: nil)
253
+ use_sudo = @use_sudo if use_sudo.nil?
254
+ return if remote_dir.nil? || remote_dir.empty? || remote_dir == '.' || remote_dir == '/'
255
+
256
+ # Create directory with -p flag to create parent directories
257
+ mkdir_command = "mkdir -p #{remote_dir.shellescape}"
258
+ execute(mkdir_command, use_sudo: use_sudo)
259
+ rescue StandardError => e
260
+ # Ignore errors if directory already exists
261
+ error_msg = e.message.downcase
262
+ unless error_msg.include?('exists') || error_msg.include?('file exists') || error_msg.include?('already exists')
263
+ raise
264
+ end
265
+ end
266
+
267
+ def delete_extra_files(source_dir, destination_dir, filter, use_sudo: nil)
268
+ use_sudo = @use_sudo if use_sudo.nil?
269
+
270
+ # Get list of remote files
271
+ list_command = "find #{destination_dir.shellescape} -type f 2>/dev/null || true"
272
+ result = execute(list_command, use_sudo: use_sudo)
273
+ remote_files = result[:stdout].lines.map(&:strip).reject(&:empty?)
274
+
275
+ # Get list of local files (relative paths)
276
+ source_path = Pathname.new(source_dir)
277
+ local_files = collect_files_to_sync(source_dir, filter).map do |file_path|
278
+ relative_path = Pathname.new(file_path).relative_path_from(source_path).to_s
279
+ File.join(destination_dir, relative_path).gsub(%r{/+}, '/')
280
+ end
281
+
282
+ # Find files to delete
283
+ files_to_delete = remote_files - local_files
284
+
285
+ # Delete extra files
286
+ deleted_count = 0
287
+ files_to_delete.each do |file_path|
288
+ delete_command = "rm -f #{file_path.shellescape}"
289
+ execute(delete_command, use_sudo: use_sudo)
290
+ deleted_count += 1
291
+ rescue StandardError
292
+ # Ignore deletion errors
293
+ end
294
+
295
+ deleted_count
296
+ end
187
297
  end
188
298
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Kdeploy
6
+ # File filter for directory synchronization
7
+ # Supports .gitignore-style patterns
8
+ class FileFilter
9
+ def initialize(ignore_patterns: [])
10
+ @ignore_patterns = normalize_patterns(ignore_patterns)
11
+ end
12
+
13
+ # Check if a file should be ignored
14
+ def ignored?(file_path, base_path = nil)
15
+ relative_path = relative_path_for(file_path, base_path)
16
+ @ignore_patterns.any? { |pattern| match_pattern?(pattern, relative_path) }
17
+ end
18
+
19
+ # Filter files from a directory
20
+ def filter_files(files, base_path = nil)
21
+ files.reject { |file| ignored?(file, base_path) }
22
+ end
23
+
24
+ private
25
+
26
+ def normalize_patterns(patterns)
27
+ patterns.map do |pattern|
28
+ normalize_pattern(pattern)
29
+ end
30
+ end
31
+
32
+ def normalize_pattern(pattern)
33
+ # Remove leading slash if present (patterns are relative to base)
34
+ pattern = pattern.sub(%r{\A/}, '')
35
+ # Convert to regex
36
+ pattern_to_regex(pattern)
37
+ end
38
+
39
+ def pattern_to_regex(pattern)
40
+ # Convert .gitignore-style pattern to regex
41
+ regex_str = pattern
42
+ .gsub('.', '\.') # Escape dots
43
+ .gsub('**', '__STAR_STAR__') # Temporarily replace **
44
+ .gsub('*', '[^/]*') # * matches anything except /
45
+ .gsub('__STAR_STAR__', '.*') # ** matches anything including /
46
+ .gsub('?', '[^/]') # ? matches single char except /
47
+ .gsub('[!', '[^') # [^...] negation
48
+ .gsub('[', '[') # Character class
49
+
50
+ # Anchor to start if pattern doesn't start with **
51
+ regex_str = "^#{regex_str}" unless pattern.start_with?('**')
52
+ # Match end of string or directory separator
53
+ regex_str = "#{regex_str}(/|$)" unless pattern.end_with?('*') || pattern.end_with?('**')
54
+
55
+ Regexp.new(regex_str)
56
+ end
57
+
58
+ def match_pattern?(pattern, file_path)
59
+ return false if file_path.nil? || file_path.empty?
60
+
61
+ pattern.match?(file_path)
62
+ end
63
+
64
+ def relative_path_for(file_path, base_path)
65
+ return file_path.to_s unless base_path
66
+
67
+ base = Pathname.new(base_path)
68
+ file = Pathname.new(file_path)
69
+ file.relative_path_from(base).to_s
70
+ end
71
+ end
72
+ end
@@ -88,6 +88,18 @@ module Kdeploy
88
88
  sudo apt-get update && sudo apt-get upgrade -y
89
89
  SHELL
90
90
  end
91
+
92
+ # Example: Directory synchronization task
93
+ task :sync_app, roles: :web do
94
+ # Sync application directory, ignoring development files
95
+ sync './app', '/var/www/app',
96
+ ignore: ['.git', '*.log', 'node_modules', '.env.local', '*.tmp'],
97
+ delete: true
98
+
99
+ # Sync configuration files
100
+ sync './config', '/etc/app',
101
+ exclude: ['*.example', '*.bak']
102
+ end
91
103
  RUBY
92
104
  end
93
105
 
@@ -262,6 +274,11 @@ module Kdeploy
262
274
  ```bash
263
275
  kdeploy execute deploy.rb update
264
276
  ```
277
+
278
+ - **sync_app**: Sync application directory to remote servers
279
+ ```bash
280
+ kdeploy execute deploy.rb sync_app
281
+ ```
265
282
  MD
266
283
  end
267
284
 
@@ -32,6 +32,17 @@ module Kdeploy
32
32
  format_file_steps(steps, shown, :upload_template, @pastel.yellow(' === Template ==='), 'upload_template: ')
33
33
  end
34
34
 
35
+ def format_sync_steps(steps, shown)
36
+ output = []
37
+ steps.each do |step|
38
+ next if step_already_shown?(step, :sync, shown)
39
+
40
+ mark_step_as_shown(step, :sync, shown)
41
+ output << format_sync_step(step)
42
+ end
43
+ output
44
+ end
45
+
35
46
  def format_file_steps(steps, shown, type, _header, prefix)
36
47
  output = []
37
48
  steps.each do |step|
@@ -154,6 +165,10 @@ module Kdeploy
154
165
  "#{@pastel.blue('>')} Upload: #{command[:source]} -> #{command[:destination]}"
155
166
  when :upload_template
156
167
  "#{@pastel.blue('>')} Template: #{command[:source]} -> #{command[:destination]}"
168
+ when :sync
169
+ ignore_str = command[:ignore]&.any? ? " (ignore: #{command[:ignore].join(', ')})" : ''
170
+ delete_str = command[:delete] ? ' (delete: true)' : ''
171
+ "#{@pastel.blue('>')} Sync: #{command[:source]} -> #{command[:destination]}#{ignore_str}#{delete_str}"
157
172
  else
158
173
  "#{@pastel.blue('>')} #{command[:type]}: #{command}"
159
174
  end
@@ -234,5 +249,24 @@ module Kdeploy
234
249
  key = [step[:command], type].hash
235
250
  shown[key] = true
236
251
  end
252
+
253
+ def format_sync_step(step)
254
+ duration_str = format_duration(step[:duration])
255
+ sync_path = step[:command].sub('sync: ', '')
256
+ # Truncate long paths for cleaner output
257
+ display_path = sync_path.length > 50 ? "...#{sync_path[-47..]}" : sync_path
258
+
259
+ result = step[:result] || {}
260
+ uploaded = result[:uploaded] || 0
261
+ deleted = result[:deleted] || 0
262
+ total = result[:total] || 0
263
+
264
+ stats = []
265
+ stats << @pastel.green("#{uploaded} uploaded") if uploaded.positive?
266
+ stats << @pastel.yellow("#{deleted} deleted") if deleted.positive?
267
+ stats_str = stats.any? ? " (#{stats.join(', ')})" : " (#{total} files)"
268
+
269
+ @pastel.dim(' 📁 ') + @pastel.cyan(display_path) + @pastel.dim(stats_str) + duration_str
270
+ end
237
271
  end
238
272
  end
@@ -162,6 +162,8 @@ module Kdeploy
162
162
  command_executor.execute_upload(command, host_name)
163
163
  when :upload_template
164
164
  command_executor.execute_upload_template(command, host_name)
165
+ when :sync
166
+ command_executor.execute_sync(command, host_name)
165
167
  else
166
168
  raise ConfigurationError, "Unknown command type: #{command[:type]}"
167
169
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Kdeploy module for version management
4
4
  module Kdeploy
5
- VERSION = '1.2.27' unless const_defined?(:VERSION)
5
+ VERSION = '1.2.28' unless const_defined?(:VERSION)
6
6
  end
data/lib/kdeploy.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'kdeploy/errors'
5
5
  require_relative 'kdeploy/configuration'
6
6
  require_relative 'kdeploy/output'
7
7
  require_relative 'kdeploy/banner'
8
+ require_relative 'kdeploy/file_filter'
8
9
  require_relative 'kdeploy/dsl'
9
10
  require_relative 'kdeploy/executor'
10
11
  require_relative 'kdeploy/command_grouper'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kdeploy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.27
4
+ version: 1.2.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kk
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-11-20 00:00:00.000000000 Z
11
+ date: 2025-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bcrypt_pbkdf
@@ -175,6 +175,7 @@ files:
175
175
  - lib/kdeploy/dsl.rb
176
176
  - lib/kdeploy/errors.rb
177
177
  - lib/kdeploy/executor.rb
178
+ - lib/kdeploy/file_filter.rb
178
179
  - lib/kdeploy/help_formatter.rb
179
180
  - lib/kdeploy/initializer.rb
180
181
  - lib/kdeploy/output.rb