Skip to content

Commit

Permalink
nextcloud#17980 - Workaround SMB Storage Driver Bug when Deleting Fol…
Browse files Browse the repository at this point in the history
…ders

For some reason, on SMBv3/Azure Files when there are more than 62 files in a folder, `RecursiveDirectoryIterator` cannot reliably iterate over the contents.
  • Loading branch information
Guy Elsmore-Paddock committed Nov 18, 2019
1 parent 5320f08 commit 959c466
Showing 1 changed file with 87 additions and 26 deletions.
113 changes: 87 additions & 26 deletions lib/private/Files/Storage/Local.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,32 +91,7 @@ public function rmdir($path) {
return false;
}
try {
$it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($this->getSourcePath($path)),
\RecursiveIteratorIterator::CHILD_FIRST
);
/**
* RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
* This bug is fixed in PHP 5.5.9 or before
* See #8376
*/
$it->rewind();
while ($it->valid()) {
/**
* @var \SplFileInfo $file
*/
$file = $it->current();
if (in_array($file->getBasename(), array('.', '..'))) {
$it->next();
continue;
} elseif ($file->isDir()) {
rmdir($file->getPathname());
} elseif ($file->isFile() || $file->isLink()) {
unlink($file->getPathname());
}
$it->next();
}
return rmdir($this->getSourcePath($path));
return $this->multipassRmdir($path);
} catch (\UnexpectedValueException $e) {
return false;
}
Expand Down Expand Up @@ -487,4 +462,90 @@ public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $t
public function writeStream(string $path, $stream, int $size = null): int {
return (int)file_put_contents($this->getSourcePath($path), $stream);
}

/**
* Attempts to clear out and delete the specified folder.
*
* This method makes multiple passes over the contents of the folder until
* either all the files inside are removed, or the contents cannot be
* deleted. This is a workaround for bugs in an underlying storage driver
* that might otherwise cause only some files within a folder to be deleted
* while others are inexplicably left behind.
*
* See:
* https://github.com/nextcloud/server/issues/17980
*
* @param string $path
* The path to the folder to remove.
*
* @return bool
* Whether or not the folder was successfully removed.
*
* @throws ForbiddenException
* If the specified path is not accessible.
*/
protected function multipassRmdir($path):bool {
$unlink_count = -1;

// Keep looping until we're no longer actually un-linking anything
// after two passes in a row.
do {
$previous_unlink_count = $unlink_count;
$unlink_count = $this->unlinkDirContents($path);
} while (($previous_unlink_count != 0) || ($unlink_count != 0));

return rmdir($this->getSourcePath($path));
}

/**
* Attempts to delete all of the files inside the given folder.
*
* @param string $path
* The path to the folder containing the files to remove.
*
* @return int
* The number of files successfully deleted.
*
* @throws ForbiddenException
* If the specified path is not accessible.
*/
protected function unlinkDirContents(string $path): int {
$total_unlink_count = 0;

$it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(
$this->getSourcePath($path),
\FilesystemIterator::SKIP_DOTS
),
\RecursiveIteratorIterator::CHILD_FIRST
);

/**
* RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
* This bug is fixed in PHP 5.5.9 or before
* See #8376
*/
$it->rewind();

while ($it->valid()) {
/* @var \SplFileInfo $file */
$file = $it->current();

if ($file->isDir()) {
$successful_unlink = rmdir($file->getPathname());
} elseif ($file->isFile() || $file->isLink()) {
$successful_unlink = @unlink($file->getPathname());
} else {
$successful_unlink = FALSE;
}

if ($successful_unlink) {
++$total_unlink_count;
}

$it->next();
}

return $total_unlink_count;
}
}

0 comments on commit 959c466

Please sign in to comment.