Add upload buttons and ZIP downloads
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/common.php';
|
||||
|
||||
$paths = unav_download_paths();
|
||||
$items = [];
|
||||
foreach ($paths as $path) {
|
||||
$items[] = unav_existing_path($path);
|
||||
}
|
||||
|
||||
if (count($items) === 1 && is_file($items[0]['path']) && !is_link($items[0]['path'])) {
|
||||
unav_stream_file($items[0]['path'], basename($items[0]['path']));
|
||||
}
|
||||
|
||||
if (!class_exists('ZipArchive')) {
|
||||
unav_download_error(500, 'ZIP support is not available');
|
||||
}
|
||||
|
||||
$zipPath = tempnam(sys_get_temp_dir(), 'u-nav-download-');
|
||||
if ($zipPath === false) {
|
||||
unav_download_error(500, 'Could not create temporary archive');
|
||||
}
|
||||
$zipFile = $zipPath . '.zip';
|
||||
rename($zipPath, $zipFile);
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipFile, ZipArchive::OVERWRITE) !== true) {
|
||||
@unlink($zipFile);
|
||||
unav_download_error(500, 'Could not create ZIP archive');
|
||||
}
|
||||
|
||||
$usedNames = [];
|
||||
foreach ($items as $item) {
|
||||
$baseName = unav_zip_unique_name(unav_zip_safe_name(basename($item['path'])), $usedNames);
|
||||
unav_zip_add_path($zip, $item['path'], $baseName);
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
$downloadName = count($items) === 1 ? unav_zip_safe_name(basename($items[0]['path'])) . '.zip' : 'u-navigator-download.zip';
|
||||
unav_stream_file($zipFile, $downloadName, true);
|
||||
|
||||
function unav_download_paths(): array {
|
||||
if (isset($_GET['paths'])) {
|
||||
$decoded = json_decode((string)$_GET['paths'], true);
|
||||
if (!is_array($decoded)) {
|
||||
unav_download_error(400, 'Invalid paths');
|
||||
}
|
||||
$paths = array_values(array_filter($decoded, fn($item) => is_string($item) && $item !== ''));
|
||||
} else {
|
||||
$paths = [$_GET['path'] ?? ''];
|
||||
}
|
||||
if (!$paths) {
|
||||
unav_download_error(400, 'Missing path');
|
||||
}
|
||||
return array_slice($paths, 0, 200);
|
||||
}
|
||||
|
||||
function unav_stream_file(string $path, string $name, bool $deleteAfter = false): void {
|
||||
if (!is_file($path) || !is_readable($path)) {
|
||||
if ($deleteAfter) {
|
||||
@unlink($path);
|
||||
}
|
||||
unav_download_error(404, 'Download file not found');
|
||||
}
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('Content-Disposition: attachment; filename="' . addcslashes($name, '"\\') . '"');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
readfile($path);
|
||||
if ($deleteAfter) {
|
||||
@unlink($path);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
function unav_zip_add_path(ZipArchive $zip, string $path, string $zipPath): void {
|
||||
if (is_link($path)) {
|
||||
return;
|
||||
}
|
||||
$real = realpath($path);
|
||||
if ($real === false) {
|
||||
return;
|
||||
}
|
||||
unav_root_for_real($real);
|
||||
|
||||
if (is_file($real)) {
|
||||
$zip->addFile($real, $zipPath);
|
||||
return;
|
||||
}
|
||||
if (!is_dir($real)) {
|
||||
return;
|
||||
}
|
||||
$zip->addEmptyDir($zipPath);
|
||||
foreach (scandir($real) ?: [] as $entry) {
|
||||
if ($entry === '.' || $entry === '..') {
|
||||
continue;
|
||||
}
|
||||
unav_zip_add_path($zip, $real . '/' . $entry, $zipPath . '/' . unav_zip_safe_name($entry));
|
||||
}
|
||||
}
|
||||
|
||||
function unav_zip_safe_name(string $name): string {
|
||||
$safe = trim(str_replace(["\0", '/', '\\'], '-', $name));
|
||||
return $safe === '' || $safe === '.' || $safe === '..' ? 'download' : $safe;
|
||||
}
|
||||
|
||||
function unav_zip_unique_name(string $name, array &$used): string {
|
||||
$candidate = $name;
|
||||
$index = 2;
|
||||
while (isset($used[$candidate])) {
|
||||
$candidate = $name . ' ' . $index;
|
||||
$index++;
|
||||
}
|
||||
$used[$candidate] = true;
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
function unav_download_error(int $status, string $message): void {
|
||||
http_response_code($status);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo $message;
|
||||
exit;
|
||||
}
|
||||
@@ -12,11 +12,22 @@ foreach ($_FILES['files']['name'] ?? [] as $index => $name) {
|
||||
if ($_FILES['files']['error'][$index] !== UPLOAD_ERR_OK) {
|
||||
unav_error(400, 'Upload failed');
|
||||
}
|
||||
$safeName = basename($name);
|
||||
$safeName = unav_upload_safe_relative_path((string)$name);
|
||||
$relativeParent = dirname($safeName);
|
||||
if ($relativeParent !== '.') {
|
||||
$parentCandidate = $target['path'] . '/' . $relativeParent;
|
||||
unav_root_for_real($target['path']);
|
||||
if (!is_dir($parentCandidate) && !mkdir($parentCandidate, 0777, true) && !is_dir($parentCandidate)) {
|
||||
unav_error(500, 'Could not create upload directory');
|
||||
}
|
||||
}
|
||||
$destination = unav_destination_path($target['path'] . '/' . $safeName);
|
||||
if (file_exists($destination['path'])) {
|
||||
unav_error(409, 'Destination already exists');
|
||||
}
|
||||
if (!is_dir(dirname($destination['path']))) {
|
||||
unav_error(500, 'Could not create upload directory');
|
||||
}
|
||||
if (!move_uploaded_file($_FILES['files']['tmp_name'][$index], $destination['path'])) {
|
||||
unav_error(500, 'Could not store uploaded file');
|
||||
}
|
||||
@@ -27,3 +38,18 @@ foreach ($_FILES['files']['name'] ?? [] as $index => $name) {
|
||||
}
|
||||
|
||||
unav_json(['uploaded' => $uploaded], 201);
|
||||
|
||||
function unav_upload_safe_relative_path(string $name): string {
|
||||
$parts = [];
|
||||
foreach (preg_split('#[/\\\\]+#', $name) ?: [] as $part) {
|
||||
$part = trim($part);
|
||||
if ($part === '' || $part === '.' || $part === '..') {
|
||||
continue;
|
||||
}
|
||||
$parts[] = str_replace("\0", '', $part);
|
||||
}
|
||||
if (!$parts) {
|
||||
unav_error(400, 'Invalid upload filename');
|
||||
}
|
||||
return implode('/', $parts);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user