175
common/Files/Response/DownloadFilesResponse.php
Executable file
175
common/Files/Response/DownloadFilesResponse.php
Executable file
@@ -0,0 +1,175 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user