Michael Cho 13be3c3073
unpack_strategy/directory: use mv for nested unpack
`mv` should preserve hardlinks and allow faster unpack on the same
filesystem. A secondary pass is done with `cp` to copy over attributes
onto any existing directories.

We only run this for nested unpacks as most direct Directory strategy
usage is for repositories where moving files breaks existing code.

This uses `cp -pR` for non-move as some potential user reported issues
could be due to Apple's `cp -l` on specific macOS versions. Can
consider re-adding `cp -l` with better handling for older macOS.
2024-10-20 11:29:30 -04:00

115 lines
3.9 KiB
Ruby

# frozen_string_literal: true
require_relative "shared_examples"
RSpec.describe UnpackStrategy::Directory do
let(:path) do
mktmpdir.tap do |path|
FileUtils.touch path/"file"
FileUtils.ln_s "file", path/"symlink"
FileUtils.ln path/"file", path/"hardlink"
FileUtils.mkdir path/"folder"
FileUtils.ln_s "folder", path/"folderSymlink"
end
end
let(:unpack_dir) { mktmpdir }
shared_examples "extract directory" do |move:|
subject(:strategy) { described_class.new(path, move:) }
it "does not follow symlinks" do
strategy.extract(to: unpack_dir)
expect(unpack_dir/"symlink").to be_a_symlink
end
it "does not follow top level symlinks to directories" do
strategy.extract(to: unpack_dir)
expect(unpack_dir/"folderSymlink").to be_a_symlink
end
it "preserves permissions of contained files" do
FileUtils.chmod 0644, path/"file"
strategy.extract(to: unpack_dir)
expect((unpack_dir/"file").stat.mode & 0777).to eq 0644
end
it "preserves permissions of contained subdirectories" do
FileUtils.mkdir unpack_dir/"folder"
FileUtils.chmod 0755, unpack_dir/"folder"
FileUtils.chmod 0700, path/"folder"
strategy.extract(to: unpack_dir)
expect((unpack_dir/"folder").stat.mode & 0777).to eq 0700
end
it "preserves permissions of the destination directory" do
FileUtils.chmod 0700, path
FileUtils.chmod 0755, unpack_dir
strategy.extract(to: unpack_dir)
expect(unpack_dir.stat.mode & 0777).to eq 0755
end
it "preserves mtime of contained files and directories" do
FileUtils.mkdir unpack_dir/"folder"
FileUtils.touch path/"folder", mtime: Time.utc(2000, 1, 2, 3, 4, 5, 678999), nocreate: true
mtimes = path.children.to_h { |child| [child.basename, child.lstat.mtime] }
strategy.extract(to: unpack_dir)
expect(unpack_dir.children.to_h { |child| [child.basename, child.lstat.mtime] }).to eq mtimes
end
it "preserves unrelated destination files and subdirectories" do
FileUtils.touch unpack_dir/"existing_file"
FileUtils.mkdir unpack_dir/"existing_folder"
strategy.extract(to: unpack_dir)
expect(unpack_dir/"existing_file").to be_a_file
expect(unpack_dir/"existing_folder").to be_a_directory
end
it "overwrites destination files/symlinks with source files/symlinks" do
FileUtils.mkdir unpack_dir/"existing_folder"
FileUtils.ln_s unpack_dir/"existing_folder", unpack_dir/"symlink"
(unpack_dir/"file").write "existing contents"
strategy.extract(to: unpack_dir)
expect((unpack_dir/"file").read).to be_empty
expect((unpack_dir/"symlink").readlink).to eq Pathname("file")
end
it "fails when overwriting a directory with a file" do
FileUtils.mkdir unpack_dir/"file"
expect { strategy.extract(to: unpack_dir) }.to raise_error(/Is a directory|cannot overwrite directory/i)
end
it "fails when overwriting a nested directory with a file" do
FileUtils.touch path/"folder/nested"
FileUtils.mkdir_p unpack_dir/"folder/nested"
expect { strategy.extract(to: unpack_dir) }.to raise_error(/Is a directory|cannot overwrite directory/i)
end
end
context "with `move: false`" do
include_examples "extract directory", move: false
end
context "with `move: true`" do
include_examples "extract directory", move: true
it "preserves hardlinks" do
strategy.extract(to: unpack_dir)
expect((unpack_dir/"file").stat.ino).to eq (unpack_dir/"hardlink").stat.ino
end
# NOTE: We don't test `move: false` because system cp behaviour is inconsistent,
# e.g. Ventura cp does not error but Sequoia and Linux cp will error
it "fails when overwriting a file with a directory" do
FileUtils.touch unpack_dir/"folder"
expect { strategy.extract(to: unpack_dir) }.to raise_error(/cannot overwrite non-directory/i)
end
end
end