Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Tag Files functions #65

Merged
merged 6 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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