Class: Metaclean::Committer

Inherits:
Object
  • Object
show all
Defined in:
lib/metaclean/committer.rb

Instance Method Summary collapse

Constructor Details

#initialize(in_place: false, dry_run: false) ⇒ Committer

Returns a new instance of Committer.



8
9
10
11
# File 'lib/metaclean/committer.rb', line 8

def initialize(in_place: false, dry_run: false)
  @in_place = in_place
  @dry_run = dry_run
end

Instance Method Details

#backup_original(final_path, source_stat) ⇒ Object



59
60
61
62
63
64
65
66
67
68
# File 'lib/metaclean/committer.rb', line 59

def backup_original(final_path, source_stat)
  backup = hardlink_with_collision_safe_name(final_path, "#{final_path}.bak")
  unless FileOps.same_identity?(File.lstat(backup), source_stat)
    File.delete(backup)
    raise Error, "#{final_path} changed during cleaning — refusing to back it up"
  end
  backup
rescue Errno::EACCES, Errno::EPERM, Errno::ENOTSUP, Errno::EMLINK, NotImplementedError => e
  raise Error, "Cannot create a metadata-preserving hard-link backup: #{e.message}; use default copy mode"
end

#build_clean_path(file) ⇒ Object



109
110
111
112
113
# File 'lib/metaclean/committer.rb', line 109

def build_clean_path(file)
  extension = File.extname(file)
  base = File.basename(file, extension)
  File.join(File.dirname(file), "#{base}#{Metaclean::CLEAN_SUFFIX}#{extension}")
end

#cleanup_staging(staging) ⇒ Object



33
34
35
36
37
38
# File 'lib/metaclean/committer.rb', line 33

def cleanup_staging(staging)
  return unless staging

  workspace = File.dirname(staging)
  FileUtils.remove_entry(workspace, true) if File.exist?(workspace)
end

#collision_safe(path) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/metaclean/committer.rb', line 115

def collision_safe(path)
  return path unless FileOps.lexist?(path)

  extension = File.extname(path)
  base = File.basename(path, extension)
  directory = File.dirname(path)
  index = 1
  loop do
    candidate = File.join(directory, "#{base}_#{index}#{extension}")
    return candidate unless FileOps.lexist?(candidate)

    index += 1
  end
end

#commit!(staging, final_path, source_stat:) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/metaclean/committer.rb', line 40

def commit!(staging, final_path, source_stat:)
  backup = nil
  committed = false
  if @in_place
    FileOps.ensure_same_file!(final_path, source_stat)
    backup = backup_original(final_path, source_stat)
    File.rename(staging, final_path)
    committed = true
    return { path: final_path, backup: backup }
  end

  { path: link_with_collision_safe_name(staging, final_path), backup: nil }
rescue SystemCallError, Interrupt
  if backup && !committed && File.exist?(staging) && FileOps.lexist?(backup)
    File.delete(backup)
  end
  raise
end

#copy_file_exclusive(source, destination, preserve: false, expected: nil) ⇒ Object



105
106
107
# File 'lib/metaclean/committer.rb', line 105

def copy_file_exclusive(source, destination, preserve: false, expected: nil)
  FileOps.copy_exclusive(source, destination, preserve: preserve, expected: expected)
end

#copy_with_collision_safe_name(source, preferred, expected: nil) ⇒ Object



95
96
97
98
99
100
101
102
103
# File 'lib/metaclean/committer.rb', line 95

def copy_with_collision_safe_name(source, preferred, expected: nil)
  target = preferred
  loop do
    copy_file_exclusive(source, target, preserve: true, expected: expected)
    return target
  rescue Errno::EEXIST
    target = collision_safe(preferred)
  end
end


70
71
72
73
74
75
76
77
78
# File 'lib/metaclean/committer.rb', line 70

def hardlink_with_collision_safe_name(source, preferred)
  target = preferred
  loop do
    File.link(source, target)
    return target
  rescue Errno::EEXIST
    target = collision_safe(preferred)
  end
end


80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/metaclean/committer.rb', line 80

def link_with_collision_safe_name(staging, preferred)
  target = preferred
  loop do
    File.link(staging, target)
    File.delete(staging)
    return target
  rescue Errno::EEXIST
    target = collision_safe(preferred)
  rescue Errno::EACCES, Errno::EPERM, Errno::ENOTSUP, Errno::EMLINK, NotImplementedError
    target = copy_with_collision_safe_name(staging, target)
    File.delete(staging)
    return target
  end
end

#resolve_final_path(file) ⇒ Object



13
14
15
16
17
# File 'lib/metaclean/committer.rb', line 13

def resolve_final_path(file)
  return file if @in_place

  collision_safe(build_clean_path(file))
end

#staging_path_for(final_path) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/metaclean/committer.rb', line 19

def staging_path_for(final_path)
  prefix = "#{Metaclean::TMP_MARKER}runner.#{Process.pid}."
  workspace = if @dry_run
                Dir.mktmpdir(prefix)
              else
                Dir.mktmpdir(prefix, File.dirname(final_path))
              end
  FileOps.secure_workspace!(workspace)
  File.join(workspace, "staging#{File.extname(final_path)}")
rescue StandardError, Interrupt
  FileUtils.remove_entry(workspace, true) if workspace && File.exist?(workspace)
  raise
end