Skip to content

Commit

Permalink
Add removeTagFile and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
whikloj committed Apr 17, 2024
1 parent 55579d0 commit a1647cf
Show file tree
Hide file tree
Showing 4 changed files with 360 additions and 33 deletions.
80 changes: 55 additions & 25 deletions src/Bag.php
Original file line number Diff line number Diff line change
Expand Up @@ -1148,16 +1148,14 @@ public function pathInBagData(string $filepath): bool
*
* @param string $path
* The file just deleted.
* @throws FilesystemException If we can't delete the directory.
* @throws BagItException If the directory is outside the data directory.
*/
public function checkForEmptyDir(string $path): void
{
$parentPath = dirname($path);
if (str_starts_with($this->makeRelative($parentPath), "data/")) {
$files = scandir($parentPath);
$payload = array_diff($files, [".", ".."]);
if (count($payload) == 0) {
rmdir($parentPath);
}
BagUtils::deleteEmptyDirTree($parentPath, $this->getDataDirectory());
}
}

Expand Down Expand Up @@ -1195,46 +1193,78 @@ public function upgrade(): void
* @param string $dest Relative path for the destination.
*
* @throws BagItException Various errors related to the source and destination locations and access.
* @throws FilesystemException Issues writing to the filesystem.
* @throws FilesystemException Issues reading from or writing to the filesystem.
*/
public function addTagFile(string $source, string $dest): void
{
if (!file_exists($source) || !is_file($source) || !is_readable($source)) {
throw new BagItException("$source does not exist, is not a file or is not readable.");
}
$this->checkTagFileConstraints($dest);
$external = $this->makeAbsolute($dest);
$relativePath = $this->makeRelative($external);
if ($relativePath === "") {
throw new BagItException("Tag files must be inside the bag root.");
}
if (str_starts_with(strtolower($relativePath), "data/")) {
throw new BagItException("Tag files must be in the bag root or a tag file directory, " .
"use ->addFile() to add data files.");
}
if (in_array(strtolower($dest), ['bagit.txt', 'bag-info.txt', 'fetch.txt'])) {
throw new BagItException("You cannot overwrite reserved file ($dest) file with your own tag file.");
} elseif (
str_starts_with(strtolower($dest), 'tagmanifest-') ||
str_starts_with(strtolower($dest), 'manifest-')
) {
throw new BagItException("You cannot overwrite a manifest or tag manifest file with your own tag file.");
}
if (file_exists($external)) {
throw new BagItException("Tag file ($dest) already exists in the bag, use ->replaceTagFile() to replace.");
}
$this->setExtended(true);
if (str_contains($relativePath, '/')) {
$parentDirs = dirname($external);
if ($parentDirs !== $this->getBagRoot() && !file_exists($parentDirs)) {
// Create any missing tag file directories.
$dir = dirname($external);
BagUtils::checkedMkdir($dir, 0777, true);
BagUtils::checkedMkdir($parentDirs, 0777, true);
}
BagUtils::checkedCopy($source, $external);
$this->changed = true;
}

/**
* Remove a tag file and any empty directories it leaves behind.
* @param string $dest The relative path to the tag file.
* @return void
* @throws BagItException If the file does not exist, is not inside the bag root or is a reserved file.
* @throws FilesystemException If there are issues deleting the file or directories.
*/
public function removeTagFile(string $dest): void
{
$this->checkTagFileConstraints($dest);
$external = $this->makeAbsolute($dest);
if (!file_exists($external)) {
throw new BagItException("Tag file ($dest) does not exist in the bag.");
}
BagUtils::checkedUnlink($external);
BagUtils::deleteEmptyDirTree(dirname($external), $this->getBagRoot());
$this->changed = true;
}

/*
* XXX: Private functions
*/

/**
* Common checks for interactions with custom tag files.
* @param string $tagFilePath The relative path to the tag file.
* @return void
* @throws BagItException If the tag file is not in the bag root, is in the data directory, or is a reserved file.
*/
private function checkTagFileConstraints(string $tagFilePath): void
{
$external = $this->makeAbsolute($tagFilePath);
$relativePath = $this->makeRelative($external);
if ($relativePath === "") {
throw new BagItException("Tag files must be inside the bag root.");
}
if (str_starts_with(strtolower($relativePath), "data/")) {
throw new BagItException("Tag files must be in the bag root or a tag file directory, " .
"use ->addFile() to add data files.");
}
if (in_array(strtolower($relativePath), ['bagit.txt', 'bag-info.txt', 'fetch.txt'])) {
throw new BagItException("You cannot alter reserved file ($tagFilePath) file with your own tag file.");
} elseif (
str_starts_with(strtolower($relativePath), 'tagmanifest-') ||
str_starts_with(strtolower($relativePath), 'manifest-')
) {
throw new BagItException("You cannot alter a manifest or tag manifest file with your own tag file.");
}
}

/**
* Load a bag from disk.
*
Expand Down
41 changes: 41 additions & 0 deletions src/BagUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace whikloj\BagItTools;

use TypeError;
use whikloj\BagItTools\Exceptions\BagItException;
use whikloj\BagItTools\Exceptions\FilesystemException;

/**
Expand Down Expand Up @@ -324,6 +325,18 @@ public static function checkedFwrite($fp, string $content): void
}
}

/**
* Remove a directory and check if it succeeded.
* @param string $path The path to remove.
* @throws FilesystemException If the call to rmdir() fails.
*/
public static function checkedRmDir(string $path): void
{
if (!@rmdir($path)) {
throw new FilesystemException("Unable to remove directory $path");
}
}

/**
* Decode a file path according to the special rules of the spec.
*
Expand Down Expand Up @@ -410,4 +423,32 @@ public static function standardizePathSeparators(string $path): string
{
return str_replace('\\', '/', $path);
}

/**
* Walk up a path as far as the rootDir and delete empty directories.
* @param string $path The path to check.
* @param string $rootDir The root to not remove .
*
* @throws BagItException If the path is not within the bag root.
* @throws FilesystemException If we can't remove a directory
*/
public static function deleteEmptyDirTree(string $path, string $rootDir): void
{
if (rtrim(strtolower($path), '/') === rtrim(strtolower($rootDir), '/')) {
return;
}
if (!str_starts_with($path, $rootDir)) {
throw new BagItException("Path is not within the root directory.");
}
if (file_exists($path) && is_dir($path)) {
$parent = dirname($path);
$files = array_diff(scandir($path), [".", ".."]);
if (count($files) === 0) {
self::checkedRmDir($path);
}
if ($parent !== $rootDir) {
self::deleteEmptyDirTree($parent, $rootDir);
}
}
}
}
93 changes: 93 additions & 0 deletions tests/BagUtilsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Exception;
use whikloj\BagItTools\BagUtils;
use whikloj\BagItTools\Exceptions\BagItException;
use whikloj\BagItTools\Exceptions\FilesystemException;

/**
Expand Down Expand Up @@ -209,6 +210,29 @@ public function testCheckedFwrite(): void
BagUtils::checkedFwrite($fp, "Some example text");
}

/**
* @covers ::checkedRmDir
*/
public function testCheckedRmDirFailure(): void
{
$this->expectException(FilesystemException::class);
$this->expectExceptionMessage("Unable to remove directory $this->tmpdir");

// try to delete a non-existent file.
BagUtils::checkedRmDir($this->tmpdir);
}

/**
* @covers ::checkedRmDir
*/
public function testCheckedRmDirSuccess(): void
{
mkdir($this->tmpdir);
$this->assertDirectoryExists($this->tmpdir);
BagUtils::checkedRmDir($this->tmpdir);
$this->assertDirectoryDoesNotExist($this->tmpdir);
}

/**
* @covers ::checkUnencodedFilepath
*/
Expand Down Expand Up @@ -239,4 +263,73 @@ public function testStandardizeFilePaths(): void
$this->assertEquals($item[0], BagUtils::standardizePathSeparators($item[1]));
}
}

/**
* @covers ::deleteEmptyDirTree
*/
public function testDeleteEmptyTreeOutsideRoot(): void
{
mkdir($this->tmpdir);
$parent = dirname($this->tmpdir);
$this->assertDirectoryExists($this->tmpdir);

$this->expectException(BagItException::class);
$this->expectExceptionMessage("Path is not within the root directory.");

BagUtils::deleteEmptyDirTree($parent, $this->tmpdir);
}

/**
* @covers ::deleteEmptyDirTree
*/
public function testDeleteMultipleLevels(): void
{
mkdir($this->tmpdir);
$bottomRung = $this->tmpdir . "/level1/level2/level3";
mkdir($bottomRung, 0777, true);
$this->assertDirectoryExists($bottomRung);
$this->assertDirectoryExists($this->tmpdir . "/level1/level2");
$this->assertDirectoryExists($this->tmpdir . "/level1");
BagUtils::deleteEmptyDirTree($bottomRung, $this->tmpdir);
$this->assertDirectoryDoesNotExist($bottomRung);
$this->assertDirectoryDoesNotExist($this->tmpdir . "/level1/level2");
$this->assertDirectoryDoesNotExist($this->tmpdir . "/level1");
}

/**
* @covers ::deleteEmptyDirTree
*/
public function testDeleteMultipleLevelsStopInMiddle(): void
{
mkdir($this->tmpdir);
$bottomRung = $this->tmpdir . "/level1/level2/level3";
$someFile = $this->tmpdir . "/level1/level2/someFile.txt";
mkdir($bottomRung, 0777, true);
// Create a file in the middle level.
touch($someFile);

$this->assertDirectoryExists($bottomRung);
$this->assertDirectoryExists($this->tmpdir . "/level1/level2");
$this->assertDirectoryExists($this->tmpdir . "/level1");
BagUtils::deleteEmptyDirTree($bottomRung, $this->tmpdir);

$this->assertDirectoryDoesNotExist($bottomRung);
// Middle level should still exist.
$this->assertDirectoryExists($this->tmpdir . "/level1/level2");
$this->assertDirectoryExists($this->tmpdir . "/level1");
// Remove the file.
unlink($someFile);

BagUtils::deleteEmptyDirTree($bottomRung, $this->tmpdir);
// Because we specified a non-existent directory, we didn't traverse anything.
$this->assertDirectoryDoesNotExist($bottomRung);
$this->assertDirectoryExists($this->tmpdir . "/level1/level2");
$this->assertDirectoryExists($this->tmpdir . "/level1");

BagUtils::deleteEmptyDirTree($this->tmpdir . "/level1/level2", $this->tmpdir);
// Now all the directories should be removed.
$this->assertDirectoryDoesNotExist($bottomRung);
$this->assertDirectoryDoesNotExist($this->tmpdir . "/level1/level2");
$this->assertDirectoryDoesNotExist($this->tmpdir . "/level1");
}
}
Loading

0 comments on commit a1647cf

Please sign in to comment.