176 lines
5.6 KiB
PHP
Executable File
176 lines
5.6 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Common\Files\Response;
|
|
|
|
use Carbon\Carbon;
|
|
use Common\Files\FileEntry;
|
|
use Illuminate\Support\Collection;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
use ZipStream\ZipStream;
|
|
|
|
class DownloadFilesResponse
|
|
{
|
|
// basename with extension => count
|
|
// for incrementing file names in zip for files that have duplicate name
|
|
protected array $filesInZip = [];
|
|
protected int $totalSize = 0;
|
|
|
|
public function __construct(
|
|
protected FileResponseFactory $fileResponseFactory,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @param Collection|FileEntry[] $entries
|
|
* @return mixed
|
|
*/
|
|
public function create($entries)
|
|
{
|
|
if ($entries->count() === 1 && $entries->first()->type !== 'folder') {
|
|
return $this->fileResponseFactory->create(
|
|
$entries->first(),
|
|
'attachment',
|
|
);
|
|
} else {
|
|
return $this->streamZip($entries);
|
|
}
|
|
}
|
|
|
|
private function streamZip(Collection $entries): StreamedResponse
|
|
{
|
|
return new StreamedResponse(
|
|
function () use ($entries) {
|
|
$timestamp = Carbon::now()->getTimestamp();
|
|
$zip = new ZipStream(
|
|
// downloading multiple files from s3 will error out without this
|
|
defaultEnableZeroHeader: true,
|
|
contentType: 'application/octet-stream',
|
|
sendHttpHeaders: true,
|
|
outputName: "download-$timestamp.zip",
|
|
);
|
|
|
|
$this->fillZip($zip, $entries);
|
|
$zip->finish();
|
|
},
|
|
200,
|
|
[
|
|
'X-Accel-Buffering' => 'no',
|
|
'Pragma' => 'public',
|
|
'Cache-Control' => 'no-cache',
|
|
'Content-Transfer-Encoding' => 'binary',
|
|
],
|
|
);
|
|
}
|
|
|
|
private function fillZip(ZipStream $zip, Collection $entries): void
|
|
{
|
|
$entries->each(function (FileEntry $entry) use ($zip) {
|
|
if ($entry->type === 'folder') {
|
|
// this will load all children, nested at any level, so no need to do a recursive loop
|
|
$entry
|
|
->allChildren()
|
|
->select([
|
|
'id',
|
|
'name',
|
|
'extension',
|
|
'path',
|
|
'type',
|
|
'file_name',
|
|
'disk_prefix',
|
|
])
|
|
->orderBy('path', 'asc')
|
|
->chunk(300, function (Collection $chunk) use (
|
|
$zip,
|
|
$entry,
|
|
) {
|
|
$chunk->each(function (FileEntry $childEntry) use (
|
|
$zip,
|
|
$entry,
|
|
$chunk,
|
|
) {
|
|
$path = $this->transformPath(
|
|
$childEntry,
|
|
$entry,
|
|
$chunk,
|
|
);
|
|
if ($childEntry->type === 'folder') {
|
|
// add empty folder in case it has no children
|
|
$zip->addFile("$path/", '');
|
|
} else {
|
|
$this->addFileToZip($childEntry, $zip, $path);
|
|
}
|
|
});
|
|
});
|
|
} else {
|
|
$this->addFileToZip($entry, $zip);
|
|
}
|
|
});
|
|
}
|
|
|
|
private function addFileToZip(
|
|
FileEntry $entry,
|
|
ZipStream $zip,
|
|
string|null $path = null,
|
|
): void {
|
|
if (!$path) {
|
|
$path = $entry->getNameWithExtension();
|
|
}
|
|
|
|
$parts = pathinfo($path);
|
|
$basename = $parts['basename'];
|
|
$filename = $parts['filename'];
|
|
$extension = $parts['extension'];
|
|
$dirname = $parts['dirname'] === '.' ? '' : $parts['dirname'];
|
|
|
|
// add number to duplicate file names (file(1).png, file(2).png etc)
|
|
if (isset($this->filesInZip[$basename])) {
|
|
$newCount = $this->filesInZip[$basename] + 1;
|
|
$this->filesInZip[$basename] = $newCount;
|
|
$path = "$dirname/$filename($newCount).$extension";
|
|
} else {
|
|
$this->filesInZip[$basename] = 0;
|
|
}
|
|
|
|
$stream = $entry->getDisk()->readStream($entry->getStoragePath());
|
|
if ($stream) {
|
|
$zip->addFileFromStream($path, $stream);
|
|
fclose($stream);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replace entry IDs with names inside "path" property.
|
|
*/
|
|
private function transformPath(
|
|
FileEntry $entry,
|
|
FileEntry $parent,
|
|
Collection $folders,
|
|
): string {
|
|
if (!$entry->path) {
|
|
return $entry->getNameWithExtension();
|
|
}
|
|
|
|
// '56/55/54 => [56,55,54]
|
|
$path = array_filter(explode('/', $entry->path));
|
|
$path = array_map(function ($id) {
|
|
return (int) $id;
|
|
}, $path);
|
|
|
|
//only generate path until specified parent and not root
|
|
$path = array_slice($path, array_search($parent->id, $path));
|
|
|
|
// last value will be id of the file itself, remove it
|
|
array_pop($path);
|
|
|
|
// map parent folder IDs to names
|
|
$path = array_map(function ($id) use ($folders, $parent) {
|
|
if ($id === $parent->id) {
|
|
return $parent->name;
|
|
}
|
|
return $folders->find($id)->name;
|
|
}, $path);
|
|
|
|
return implode('/', $path) . '/' . $entry->getNameWithExtension();
|
|
}
|
|
}
|