dotsync 0.1.19 → 0.1.20

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: 296888a8438bcbc159e621b7e1423429c6a937e7d67ec8970c4b0779d85b840f
4
- data.tar.gz: 2d6fa4363f2a107cd074002ef8d40bb553cf5aa5bb11f52048ec3eb58cf90a09
3
+ metadata.gz: b3c840b83736169eaabf3296862872f088fc9ae49dc15545adf49bf75439620e
4
+ data.tar.gz: c8cf364077464a687df689c9bc6f43aa75fa45c61df531fcdc498a043ab2b02d
5
5
  SHA512:
6
- metadata.gz: 389498f088842bebc6a45426d1451b9ac5cd00724526fcf92633d7dbf48ad32adc35986067ade21d086963d557a993b58d5a35c6332b5ef6e81ec36065f81600
7
- data.tar.gz: 740404a8803dcc05f053c0f1b7ba57f1187b7b920a8f34af3217162c554e8d263740bad31921bf8249e9b25820a6905651c200da63e8e072573f031d775516cf
6
+ metadata.gz: 105b36727336e5169a23de08a2a8f2165a568682a8ea4f590f93d93525990181cf3994239151d9a1e72d75e33416c217777a604373a37fbd0d7ff3e1aca94d93
7
+ data.tar.gz: 97a767742387ebd6af332e8e1a1d4e19b887c019fe5fb4863d449ea9e9ba6c9ddcf8459bee0a018362761d7f2c4d699bd7b69240af45ebc144c9ded697d46115
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ # 0.1.20
2
+
3
+ **Robustness & Error Handling:**
4
+ - Add specific error classes for better error handling (`PermissionError`, `DiskFullError`, `SymlinkError`, `TypeConflictError`)
5
+ - Add symlink support with proper preservation of link targets (regular, broken, and relative symlinks)
6
+ - Add type conflict detection to prevent overwriting directories with files or vice versa
7
+ - Enhance FileTransfer error handling for permission issues and disk space errors
8
+
9
+ **Testing & Quality:**
10
+ - Add 16 new test cases covering edge cases and error scenarios
11
+ - Add comprehensive symlink handling tests (regular, broken, relative)
12
+ - Add path traversal security validation tests
13
+ - Add Unicode filename compatibility tests (Russian, Japanese, Chinese, emoji)
14
+ - Add empty directory transfer tests
15
+ - Add Mapping#apply_to tests for path handling and force flag preservation
16
+ - Improve content comparison tests to verify actual file changes
17
+ - Improve path validation tests with more edge cases
18
+ - Total test count increased from 136 to 152 examples
19
+
20
+ **Developer Experience:**
21
+ - All tests passing (152 examples, 0 failures)
22
+ - RuboCop compliant with no offenses
23
+
1
24
  # 0.1.19
2
25
 
3
26
  **Documentation & Testing:**
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dotsync (0.1.19)
4
+ dotsync (0.1.20)
5
5
  fileutils (~> 1.7.3)
6
6
  find (~> 0.2.0)
7
7
  listen (~> 3.9.0)
@@ -3,4 +3,9 @@
3
3
  module Dotsync
4
4
  class Error < StandardError; end
5
5
  class ConfigError < StandardError; end
6
+ class FileTransferError < Error; end
7
+ class PermissionError < FileTransferError; end
8
+ class DiskFullError < FileTransferError; end
9
+ class SymlinkError < FileTransferError; end
10
+ class TypeConflictError < FileTransferError; end
6
11
  end
@@ -58,11 +58,17 @@ module Dotsync
58
58
  end
59
59
 
60
60
  def valid?
61
+ return false unless paths_are_distinct?
62
+ return false unless paths_not_nested?
61
63
  directories? || files? || file_present_in_src_only?
62
64
  end
63
65
 
64
66
  def file_changed?
65
- files_present? && (File.size(src) != File.size(dest))
67
+ return false unless files_present?
68
+ # Check size first for quick comparison
69
+ return true if File.size(src) != File.size(dest)
70
+ # If sizes match, compare content
71
+ FileUtils.compare_file(src, dest) == false
66
72
  end
67
73
 
68
74
  def backup_possible?
@@ -154,5 +160,16 @@ module Dotsync
154
160
  end
155
161
  [sanitized_src, sanitized_dest, sanitized_ignores, sanitized_only]
156
162
  end
163
+
164
+ def paths_are_distinct?
165
+ src != dest
166
+ end
167
+
168
+ def paths_not_nested?
169
+ # Check if dest is inside src or vice versa
170
+ return false if dest.start_with?("#{src}/")
171
+ return false if src.start_with?("#{dest}/")
172
+ true
173
+ end
157
174
  end
158
175
  end
@@ -46,7 +46,7 @@ module Dotsync
46
46
  if !File.exist?(dest_path)
47
47
  additions << rel_path
48
48
  elsif File.file?(src_path) && File.file?(dest_path)
49
- if File.size(src_path) != File.size(dest_path)
49
+ if files_differ?(src_path, dest_path)
50
50
  modifications << rel_path
51
51
  end
52
52
  end
@@ -92,5 +92,13 @@ module Dotsync
92
92
  end
93
93
  end
94
94
  end
95
+
96
+ def files_differ?(src_path, dest_path)
97
+ # First check size for quick comparison
98
+ return true if File.size(src_path) != File.size(dest_path)
99
+
100
+ # If sizes match, compare content
101
+ FileUtils.compare_file(src_path, dest_path) == false
102
+ end
95
103
  end
96
104
  end
@@ -20,7 +20,28 @@ module Dotsync
20
20
 
21
21
  def transfer
22
22
  if File.file?(@src)
23
- transfer_file(@src, @dest)
23
+ # Check if we're trying to overwrite a directory with a file
24
+ if File.exist?(@dest) && File.directory?(@dest) && !File.symlink?(@dest)
25
+ # If @dest is a directory and NOT just a parent directory for the file,
26
+ # this is a conflict. The check is: if @dest path exactly matches where
27
+ # we want the file to be (not a parent dir), then it's a conflict.
28
+ # We determine this by checking if File.basename(@src) already appears
29
+ # to be accounted for in @dest path.
30
+ dest_basename = File.basename(@dest)
31
+ src_basename = File.basename(@src)
32
+
33
+ if dest_basename == src_basename
34
+ raise Dotsync::TypeConflictError, "Cannot overwrite directory '#{@dest}' with file '#{@src}'"
35
+ end
36
+ end
37
+
38
+ # If dest is a directory, compute the target file path
39
+ target_dest = if File.directory?(@dest)
40
+ File.join(@dest, File.basename(@src))
41
+ else
42
+ @dest
43
+ end
44
+ transfer_file(@src, target_dest)
24
45
  else
25
46
  cleanup_folder(@dest) if @force
26
47
  transfer_folder(@src, @dest)
@@ -31,8 +52,29 @@ module Dotsync
31
52
  attr_reader :mapping, :ignores
32
53
 
33
54
  def transfer_file(file_src, file_dest)
55
+ # Check for type conflicts before transfer
56
+ if File.exist?(file_dest) && File.directory?(file_dest)
57
+ raise Dotsync::TypeConflictError, "Cannot overwrite directory '#{file_dest}' with file '#{file_src}'"
58
+ end
59
+
34
60
  FileUtils.mkdir_p(File.dirname(file_dest))
35
- FileUtils.cp(file_src, file_dest)
61
+
62
+ # Use atomic write: copy to temp file, then rename
63
+ # This prevents corruption if copy is interrupted
64
+ temp_file = "#{file_dest}.tmp.#{Process.pid}"
65
+ begin
66
+ FileUtils.cp(file_src, temp_file)
67
+ FileUtils.mv(temp_file, file_dest, force: true)
68
+ rescue Errno::EACCES, Errno::EPERM => e
69
+ FileUtils.rm_f(temp_file) if File.exist?(temp_file)
70
+ raise Dotsync::PermissionError, "Permission denied: #{e.message}"
71
+ rescue Errno::ENOSPC => e
72
+ FileUtils.rm_f(temp_file) if File.exist?(temp_file)
73
+ raise Dotsync::DiskFullError, "Disk full: #{e.message}"
74
+ rescue StandardError => e
75
+ FileUtils.rm_f(temp_file) if File.exist?(temp_file)
76
+ raise Dotsync::FileTransferError, "Transfer failed: #{e.message}"
77
+ end
36
78
  end
37
79
 
38
80
  def transfer_folder(folder_src, folder_dest)
@@ -53,14 +95,42 @@ module Dotsync
53
95
  next if mapping.ignore?(full_path)
54
96
 
55
97
  target = File.join(folder_dest, File.basename(path))
56
- if File.file?(full_path)
57
- FileUtils.cp(full_path, target)
58
- else
98
+ if File.symlink?(full_path)
99
+ transfer_symlink(full_path, target)
100
+ elsif File.file?(full_path)
101
+ transfer_file(full_path, target)
102
+ elsif File.directory?(full_path)
59
103
  transfer_folder(full_path, target)
60
104
  end
61
105
  end
62
106
  end
63
107
 
108
+ def transfer_symlink(symlink_src, symlink_dest)
109
+ # Check if we're trying to overwrite a regular file or directory with a symlink
110
+ if File.exist?(symlink_dest) && !File.symlink?(symlink_dest)
111
+ if File.directory?(symlink_dest)
112
+ raise Dotsync::TypeConflictError, "Cannot overwrite directory '#{symlink_dest}' with symlink '#{symlink_src}'"
113
+ end
114
+ end
115
+
116
+ FileUtils.mkdir_p(File.dirname(symlink_dest))
117
+
118
+ # Get the target the symlink points to
119
+ link_target = File.readlink(symlink_src)
120
+
121
+ begin
122
+ # Remove existing symlink if present
123
+ FileUtils.rm(symlink_dest) if File.exist?(symlink_dest) || File.symlink?(symlink_dest)
124
+
125
+ # Create the new symlink
126
+ File.symlink(link_target, symlink_dest)
127
+ rescue Errno::EACCES, Errno::EPERM => e
128
+ raise Dotsync::PermissionError, "Permission denied creating symlink: #{e.message}"
129
+ rescue StandardError => e
130
+ raise Dotsync::SymlinkError, "Failed to create symlink: #{e.message}"
131
+ end
132
+ end
133
+
64
134
  def cleanup_folder(target_dir)
65
135
  target_dir = File.expand_path(target_dir)
66
136
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dotsync
4
- VERSION = "0.1.19"
4
+ VERSION = "0.1.20"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dotsync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.19
4
+ version: 0.1.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Sáenz