Skip to content

Commit

Permalink
Add Tag Files functions (#65)
Browse files Browse the repository at this point in the history
* Add addTagFile()

* Add removeTagFile and tests

* Build pushed feature branches

* Update action versions

* Fix messages

* Cover the new private function shared by addTagFile and removeTagFile
  • Loading branch information
whikloj authored Apr 17, 2024
1 parent d91cf60 commit b831fc9
Show file tree
Hide file tree
Showing 9 changed files with 533 additions and 37 deletions.
11 changes: 7 additions & 4 deletions .github/workflows/v5.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
pull_request:
branches:
- "v5"
push:
branches:
- "feature/v5/*"

jobs:
build:
Expand All @@ -19,7 +22,7 @@ jobs:
name: PHP ${{ matrix.php-versions }} - OS ${{ matrix.host-os }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand All @@ -35,7 +38,7 @@ jobs:
if: ${{ startsWith( matrix.host-os , 'ubuntu') }}

- name: Cache dependencies (Ubuntu)
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ steps.composercache-ubuntu.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
Expand All @@ -50,7 +53,7 @@ jobs:
if: ${{ startsWith( matrix.host-os , 'windows') }}

- name: Cache dependencies (Windows)
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ steps.composercache-windows.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
Expand All @@ -68,7 +71,7 @@ jobs:
run: composer phpunit

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
file: ./clover.xml
fail_ci_if_error: true
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"./vendor/bin/phpcpd --suffix='.php' src"
],
"phpunit": [
"phpdbg -qrr ./vendor/bin/phpunit -d memory_limit=-1 --testsuite BagIt"
"phpdbg -qrr ./vendor/bin/phpunit -d memory_limit=-1 --verbose --testsuite BagIt"
],
"test": [
"@check",
Expand Down
96 changes: 82 additions & 14 deletions src/Bag.php
Original file line number Diff line number Diff line change
Expand Up @@ -670,9 +670,7 @@ public function removeBagInfoTagValue(string $tag, string $value, bool $case_sen
*/
public function addBagInfoTag(string $tag, string $value): void
{
if (!$this->isExtended) {
throw new BagItException("This bag is not extended, you need '\$bag->setExtended(true);'");
}
$this->setExtended(true);
$internal_tag = self::trimLower($tag);
if (in_array($internal_tag, self::BAG_INFO_GENERATED_ELEMENTS)) {
throw new BagItException("Field $tag is auto-generated and cannot be manually set.");
Expand All @@ -690,11 +688,9 @@ public function addBagInfoTag(string $tag, string $value): void
*/
public function addBagInfoTags(array $tags): void
{
if (!$this->isExtended) {
throw new BagItException("This bag is not extended, you need '\$bag->setExtended(true);'");
}
$this->setExtended(true);
$normalized_keys = array_keys($tags);
$normalized_keys = array_map('self::trimLower', $normalized_keys);
$normalized_keys = array_map(self::class . '::trimLower', $normalized_keys);
$overlap = array_intersect($normalized_keys, self::BAG_INFO_GENERATED_ELEMENTS);
if (count($overlap) !== 0) {
throw new BagItException(
Expand Down Expand Up @@ -892,10 +888,10 @@ public function setAlgorithm(string $algorithm): void
*/
public function setAlgorithms(array $algorithms): void
{
$internal_names = array_map('self::getHashName', $algorithms);
$internal_names = array_map(self::class . '::getHashName', $algorithms);
$valid_algorithms = array_filter($internal_names, [$this, 'hashIsSupported']);
if (count($valid_algorithms) !== count($algorithms)) {
throw new BagItException("One or more of the algorithms provided are supported.");
throw new BagItException("One or more of the algorithms provided are NOT supported.");
}
$this->setAlgorithmsInternal($valid_algorithms);
}
Expand Down Expand Up @@ -1152,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 @@ -1193,10 +1187,84 @@ public function upgrade(): void
$this->update();
}

/**
* Add a special tag file to the bag.
* @param string $source Full path to the tag file.
* @param string $dest Relative path for the destination.
*
* @throws BagItException Various errors related to the source and destination locations and access.
* @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);
if (file_exists($external)) {
throw new BagItException("Tag file ($dest) already exists in the bag.");
}
$this->setExtended(true);
$parentDirs = dirname($external);
if ($parentDirs !== $this->getBagRoot() && !file_exists($parentDirs)) {
// Create any missing tag file directories.
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);
}
}
}
}
12 changes: 6 additions & 6 deletions tests/BagTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ public function testSetAlgorithmsFailure(): void
$bag = Bag::create($this->tmpdir);
$this->assertArrayEquals(['sha512'], $bag->getAlgorithms());
$this->expectException(BagItException::class);
$this->expectExceptionMessage("One or more of the algorithms provided are supported.");
$this->expectExceptionMessage("One or more of the algorithms provided are NOT supported.");
$bag->setAlgorithms(['sha1', 'SHA-224', "bad-algorithm"]);
}

Expand Down Expand Up @@ -1013,17 +1013,17 @@ public function testFailOnEncodedBagIt(): void
}

/**
* Test that for a non-extended bag, trying to add bag-info tags throws an error.
* Test that for a non-extended bag, trying to add bag-info tags no longer throws an error.
* @group Bag
* @covers ::addBagInfoTag
*/
public function testAddBagInfoWhenNotExtended(): void
{
$this->expectException(BagItException::class);
$this->expectExceptionMessage("This bag is not extended, you need '\$bag->setExtended(true);'");

$bag = Bag::create($this->tmpdir);
$bag->addBagInfoTag("Contact-Name", "Jared Whiklo");
$this->assertFalse($bag->isExtended());
$bag->addBagInfoTag("Contact-Name", "Bob Smith");
$this->assertTrue($bag->isExtended());
$this->assertArrayEquals(["Bob Smith"], $bag->getBagInfoByTag("Contact-Name"));
}

/**
Expand Down
Loading

0 comments on commit b831fc9

Please sign in to comment.