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 +4 -4
- data/CHANGELOG.md +23 -0
- data/Gemfile.lock +1 -1
- data/lib/dotsync/errors.rb +5 -0
- data/lib/dotsync/models/mapping.rb +18 -1
- data/lib/dotsync/utils/directory_differ.rb +9 -1
- data/lib/dotsync/utils/file_transfer.rb +75 -5
- data/lib/dotsync/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3c840b83736169eaabf3296862872f088fc9ae49dc15545adf49bf75439620e
|
|
4
|
+
data.tar.gz: c8cf364077464a687df689c9bc6f43aa75fa45c61df531fcdc498a043ab2b02d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/lib/dotsync/errors.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
data/lib/dotsync/version.rb
CHANGED