40
common/Files/Tus/DeleteExpiredTusUploads.php
Executable file
40
common/Files/Tus/DeleteExpiredTusUploads.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Tus;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class DeleteExpiredTusUploads extends Command
|
||||
{
|
||||
protected $signature = 'tus:abort_expired';
|
||||
|
||||
protected $description = 'Abort and delete expired TUS uploads.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$cache = app(TusCache::class);
|
||||
|
||||
$directory = storage_path('tus');
|
||||
|
||||
if (File::exists($directory)) {
|
||||
foreach (File::allFiles(storage_path('tus')) as $file) {
|
||||
$uploadKey = $file->getFilename();
|
||||
$tusData = $cache->get($uploadKey);
|
||||
if (
|
||||
!Arr::get($tusData, 'expires_at') ||
|
||||
Carbon::parse($tusData['expires_at'])->lt(Carbon::now())
|
||||
) {
|
||||
$cache->delete($uploadKey);
|
||||
File::delete($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Expired TUS uploads deleted.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
7
common/Files/Tus/Exceptions/ConnectionException.php
Executable file
7
common/Files/Tus/Exceptions/ConnectionException.php
Executable file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Tus\Exceptions;
|
||||
|
||||
class ConnectionException extends \Exception
|
||||
{
|
||||
}
|
||||
7
common/Files/Tus/Exceptions/FileException.php
Executable file
7
common/Files/Tus/Exceptions/FileException.php
Executable file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Tus\Exceptions;
|
||||
|
||||
class FileException extends \RuntimeException
|
||||
{
|
||||
}
|
||||
7
common/Files/Tus/Exceptions/OutOfRangeException.php
Executable file
7
common/Files/Tus/Exceptions/OutOfRangeException.php
Executable file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Tus\Exceptions;
|
||||
|
||||
class OutOfRangeException extends \OutOfRangeException
|
||||
{
|
||||
}
|
||||
47
common/Files/Tus/TusCache.php
Executable file
47
common/Files/Tus/TusCache.php
Executable file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Tus;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
|
||||
class TusCache
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
if (
|
||||
config('cache.default') === 'array' ||
|
||||
config('cache.default') === 'null'
|
||||
) {
|
||||
config()->set('cache.default', 'file');
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $uploadKey)
|
||||
{
|
||||
return cache()->get("tus:$uploadKey");
|
||||
}
|
||||
|
||||
public function set(
|
||||
string $uploadKey,
|
||||
array $value,
|
||||
CarbonInterface $expiresAt,
|
||||
): bool {
|
||||
return cache()->set("tus:$uploadKey", $value, $expiresAt);
|
||||
}
|
||||
|
||||
public function merge(string $uploadKey, array $partialValue): bool
|
||||
{
|
||||
$value = $this->get($uploadKey);
|
||||
return cache()->set(
|
||||
"tus:$uploadKey",
|
||||
array_merge($value, $partialValue),
|
||||
Carbon::parse($value['expires_at']),
|
||||
);
|
||||
}
|
||||
|
||||
public function delete(string $uploadKey): bool
|
||||
{
|
||||
return cache()->delete("tus:$uploadKey");
|
||||
}
|
||||
}
|
||||
158
common/Files/Tus/TusFile.php
Executable file
158
common/Files/Tus/TusFile.php
Executable file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Tus;
|
||||
|
||||
use Common\Files\Tus\Exceptions\ConnectionException;
|
||||
use Common\Files\Tus\Exceptions\FileException;
|
||||
use Common\Files\Tus\Exceptions\OutOfRangeException;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class TusFile
|
||||
{
|
||||
protected const INPUT_STREAM = 'php://input';
|
||||
protected const READ_BINARY = 'rb';
|
||||
protected const APPEND_BINARY = 'ab';
|
||||
|
||||
protected int $offset = 0;
|
||||
protected int $totalBytes = 0;
|
||||
protected string $uploadKey;
|
||||
protected string $filePath;
|
||||
protected TusCache $cache;
|
||||
|
||||
public function __construct(array $params)
|
||||
{
|
||||
$this->cache = app(TusCache::class);
|
||||
$this->uploadKey = $params['upload_key'];
|
||||
$this->totalBytes = $params['total_bytes'];
|
||||
$this->filePath = $params['file_path'];
|
||||
$this->offset = $params['offset'];
|
||||
}
|
||||
|
||||
public function upload(): int
|
||||
{
|
||||
if ($this->offset === $this->totalBytes) {
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
$method = config('common.site.uploads_tus_method') ?: 'wait';
|
||||
|
||||
$input = $this->open(self::INPUT_STREAM, self::READ_BINARY);
|
||||
$output = $this->open($this->filePath, self::APPEND_BINARY);
|
||||
|
||||
try {
|
||||
if ($method === 'loop') {
|
||||
$this->uploadUsingLoop($input, $output);
|
||||
} else {
|
||||
$this->uploadUsingCopyToStream($input, $output);
|
||||
}
|
||||
} finally {
|
||||
$this->cache->merge($this->uploadKey, [
|
||||
'offset' => $this->offset,
|
||||
]);
|
||||
fclose($input);
|
||||
fclose($output);
|
||||
}
|
||||
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $input
|
||||
* @param resource $output
|
||||
*/
|
||||
protected function uploadUsingLoop($input, $output): void
|
||||
{
|
||||
$chunkSize = 2097152; // 2MB
|
||||
$this->seek($output, $this->offset);
|
||||
|
||||
while (!feof($input)) {
|
||||
if (CONNECTION_NORMAL !== connection_status()) {
|
||||
throw new ConnectionException('Connection aborted by user.');
|
||||
}
|
||||
|
||||
// read
|
||||
$data = fread($input, $chunkSize);
|
||||
if ($data === false) {
|
||||
throw new FileException('Cannot read file.');
|
||||
}
|
||||
|
||||
// write
|
||||
$bytesWritten = fwrite($output, $data, $chunkSize);
|
||||
if ($bytesWritten === false) {
|
||||
throw new FileException('Cannot write to a file.');
|
||||
}
|
||||
|
||||
$this->offset += $bytesWritten;
|
||||
|
||||
if ($this->offset > $this->totalBytes) {
|
||||
throw new OutOfRangeException('The uploaded file is corrupt.');
|
||||
}
|
||||
|
||||
if ($this->offset === $this->totalBytes) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $input
|
||||
* @param resource $output
|
||||
*/
|
||||
protected function uploadUsingCopyToStream($input, $output): void
|
||||
{
|
||||
$this->seek($output, $this->offset);
|
||||
|
||||
$bytesWritten = stream_copy_to_stream($input, $output, null);
|
||||
|
||||
if ($bytesWritten === false) {
|
||||
throw new FileException('Cannot write to a file.');
|
||||
}
|
||||
|
||||
$this->offset += $bytesWritten;
|
||||
|
||||
if ($this->offset > $this->totalBytes) {
|
||||
throw new OutOfRangeException('The uploaded file is corrupt.');
|
||||
}
|
||||
}
|
||||
|
||||
protected function open(string $filePath, string $mode)
|
||||
{
|
||||
$this->exists($filePath, $mode);
|
||||
|
||||
$resource = @fopen($filePath, $mode);
|
||||
|
||||
if ($resource === false) {
|
||||
throw new FileException("Unable to open $filePath.");
|
||||
}
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public function exists(
|
||||
string $filePath,
|
||||
string $mode = self::READ_BINARY,
|
||||
): bool {
|
||||
if (self::INPUT_STREAM === $filePath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
File::ensureDirectoryExists(storage_path('tus'));
|
||||
|
||||
if (self::READ_BINARY === $mode && !file_exists($filePath)) {
|
||||
throw new FileException('File not found.');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function seek($handle, int $offset): int
|
||||
{
|
||||
$position = fseek($handle, $offset);
|
||||
|
||||
if (-1 === $position) {
|
||||
throw new FileException('Cannot move pointer to desired position.');
|
||||
}
|
||||
|
||||
return $position;
|
||||
}
|
||||
}
|
||||
52
common/Files/Tus/TusFileEntryController.php
Executable file
52
common/Files/Tus/TusFileEntryController.php
Executable file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Tus;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
use Common\Files\Actions\CreateFileEntry;
|
||||
use Common\Files\Actions\StoreFile;
|
||||
use Common\Files\Events\FileUploaded;
|
||||
use Common\Files\FileEntry;
|
||||
use Common\Files\FileEntryPayload;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class TusFileEntryController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$data = $this->validate(request(), [
|
||||
'uploadKey' => 'required|string',
|
||||
]);
|
||||
|
||||
$tusData = app(TusCache::class)->get($data['uploadKey']);
|
||||
|
||||
if (!$tusData) {
|
||||
return $this->error();
|
||||
}
|
||||
|
||||
$metadata = $tusData['metadata'];
|
||||
$tusFilePath = $tusData['file_path'];
|
||||
$metadata['size'] = $tusData['size'];
|
||||
// tus temp file fingerprint, not needed anymore
|
||||
unset($metadata['name']);
|
||||
|
||||
$payload = new FileEntryPayload($metadata);
|
||||
|
||||
$this->authorize('store', [FileEntry::class, $payload->parentId]);
|
||||
|
||||
app(StoreFile::class)->execute($payload, [
|
||||
'path' => $tusFilePath,
|
||||
'moveFile' => true,
|
||||
]);
|
||||
|
||||
$fileEntry = app(CreateFileEntry::class)->execute($payload);
|
||||
event(new FileUploaded($fileEntry));
|
||||
File::delete($tusFilePath);
|
||||
return $this->success(['fileEntry' => $fileEntry]);
|
||||
}
|
||||
}
|
||||
377
common/Files/Tus/TusServer.php
Executable file
377
common/Files/Tus/TusServer.php
Executable file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Tus;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Common\Files\Actions\ValidateFileUpload;
|
||||
use Common\Settings\Settings;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use TusPhp\Exception\ConnectionException;
|
||||
use TusPhp\Exception\FileException;
|
||||
use TusPhp\Exception\OutOfRangeException;
|
||||
|
||||
class TusServer
|
||||
{
|
||||
protected TusCache $cache;
|
||||
protected array $allowedHttpVerbs = [
|
||||
Request::METHOD_POST,
|
||||
Request::METHOD_PATCH,
|
||||
Request::METHOD_DELETE,
|
||||
Request::METHOD_HEAD,
|
||||
Request::METHOD_OPTIONS,
|
||||
];
|
||||
|
||||
protected const TUS_EXTENSIONS = [
|
||||
'creation',
|
||||
'termination',
|
||||
'checksum',
|
||||
'expiration',
|
||||
];
|
||||
|
||||
protected const TUS_PROTOCOL_VERSION = '1.0.0';
|
||||
protected const HEADER_CONTENT_TYPE = 'application/offset+octet-stream';
|
||||
protected const DEFAULT_CHECKSUM_ALGORITHM = 'sha256';
|
||||
protected const HTTP_CHECKSUM_MISMATCH = 460;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->cache = app(TusCache::class);
|
||||
}
|
||||
|
||||
public function serve(): Response
|
||||
{
|
||||
$requestMethod = request()->method();
|
||||
|
||||
if (!in_array($requestMethod, $this->allowedHttpVerbs, true)) {
|
||||
return $this->response(null, Response::HTTP_METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
$clientVersion = request()->header('Tus-Resumable');
|
||||
|
||||
if (
|
||||
Request::METHOD_OPTIONS !== $requestMethod &&
|
||||
$clientVersion &&
|
||||
self::TUS_PROTOCOL_VERSION !== $clientVersion
|
||||
) {
|
||||
return $this->response(null, Response::HTTP_PRECONDITION_FAILED, [
|
||||
'Tus-Version' => self::TUS_PROTOCOL_VERSION,
|
||||
]);
|
||||
}
|
||||
|
||||
return match (strtoupper($requestMethod)) {
|
||||
'OPTIONS' => $this->handleOptions(),
|
||||
'HEAD' => $this->handleHead(),
|
||||
'POST' => $this->handlePost(),
|
||||
'PATCH' => $this->handlePatch(),
|
||||
'DELETE' => $this->handleDelete(),
|
||||
};
|
||||
}
|
||||
|
||||
protected function handleOptions(): Response
|
||||
{
|
||||
$supportedAlgorithms = hash_algos();
|
||||
|
||||
$algorithms = [];
|
||||
foreach ($supportedAlgorithms as $hashAlgo) {
|
||||
if (str_contains($hashAlgo, ',')) {
|
||||
$algorithms[] = "'{$hashAlgo}'";
|
||||
} else {
|
||||
$algorithms[] = $hashAlgo;
|
||||
}
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'Allow' => implode(',', $this->allowedHttpVerbs),
|
||||
'Tus-Version' => self::TUS_PROTOCOL_VERSION,
|
||||
'Tus-Extension' => implode(',', self::TUS_EXTENSIONS),
|
||||
'Tus-Checksum-Algorithm' => implode(',', $algorithms),
|
||||
];
|
||||
|
||||
$maxUploadSize = app(Settings::class)->get('uploads.max_size');
|
||||
if ($maxUploadSize > 0) {
|
||||
$headers['Tus-Max-Size'] = $maxUploadSize;
|
||||
}
|
||||
|
||||
return $this->response(null, Response::HTTP_OK, $headers);
|
||||
}
|
||||
|
||||
protected function handleHead(): Response
|
||||
{
|
||||
$uploadKey = $this->getUploadKeyFromUrl();
|
||||
|
||||
if (!($tusData = $this->cache->get($uploadKey))) {
|
||||
return $this->response(null, Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$offset = $tusData['offset'] ?? false;
|
||||
|
||||
if ($offset === false) {
|
||||
return $this->response(null, Response::HTTP_GONE);
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'Upload-Length' => (int) $tusData['size'],
|
||||
'Upload-Offset' => (int) $tusData['offset'],
|
||||
'Cache-Control' => 'no-store',
|
||||
];
|
||||
|
||||
return $this->response(null, Response::HTTP_OK, $headers);
|
||||
}
|
||||
|
||||
protected function handlePost(): Response
|
||||
{
|
||||
$meta = $this->extractMeta();
|
||||
$errors = app(ValidateFileUpload::class)->execute([
|
||||
'size' => $meta['clientSize'],
|
||||
'extension' => $meta['clientExtension'],
|
||||
]);
|
||||
|
||||
if ($errors) {
|
||||
return $this->response(
|
||||
json_encode(['message' => $errors->first()]),
|
||||
Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
);
|
||||
}
|
||||
|
||||
$uploadKey = $this->getOrCreateUploadKey();
|
||||
$filePath = storage_path("tus/$uploadKey");
|
||||
|
||||
$checksum = $this->getClientChecksum();
|
||||
$location = url("api/v1/tus/upload/$uploadKey");
|
||||
|
||||
$expiresAt = now()->addDay();
|
||||
$formattedExpiresAt = $expiresAt->format('D, d M Y H:i:s \G\M\T');
|
||||
$this->cache->set(
|
||||
$uploadKey,
|
||||
[
|
||||
'size' => (int) request()->header('Upload-Length'),
|
||||
'offset' => 0,
|
||||
'checksum' => $checksum,
|
||||
'location' => $location,
|
||||
'file_path' => $filePath,
|
||||
'metadata' => $this->extractMeta(),
|
||||
'created_at' => Carbon::now()->timestamp,
|
||||
'expires_at' => $formattedExpiresAt,
|
||||
],
|
||||
$expiresAt,
|
||||
);
|
||||
|
||||
return $this->response(null, Response::HTTP_CREATED, [
|
||||
'Location' => $location,
|
||||
'Upload-Expires' => $formattedExpiresAt,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function handlePatch(): Response
|
||||
{
|
||||
$uploadKey = $this->getUploadKeyFromUrl();
|
||||
|
||||
if (!($tusData = $this->cache->get($uploadKey))) {
|
||||
return $this->response(null, Response::HTTP_GONE);
|
||||
}
|
||||
|
||||
$status = $this->verifyPatchRequest($tusData);
|
||||
if (Response::HTTP_OK !== $status) {
|
||||
return $this->response(null, $status);
|
||||
}
|
||||
|
||||
$checksum = $tusData['checksum'];
|
||||
$fileSize = $tusData['size'];
|
||||
|
||||
try {
|
||||
$newOffset = (new TusFile([
|
||||
'upload_key' => $this->getUploadKeyFromUrl(),
|
||||
'total_bytes' => $tusData['size'],
|
||||
'file_path' => $tusData['file_path'],
|
||||
'offset' => $tusData['offset'],
|
||||
]))->upload();
|
||||
|
||||
if (
|
||||
$newOffset === $fileSize &&
|
||||
!$this->verifyChecksum($checksum, $tusData['file_path'])
|
||||
) {
|
||||
return $this->response(null, self::HTTP_CHECKSUM_MISMATCH);
|
||||
}
|
||||
} catch (FileException $e) {
|
||||
return $this->response(
|
||||
$e->getMessage(),
|
||||
Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
);
|
||||
} catch (OutOfRangeException) {
|
||||
return $this->response(
|
||||
null,
|
||||
Response::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE,
|
||||
);
|
||||
} catch (ConnectionException) {
|
||||
return $this->response(null, Response::HTTP_CONTINUE);
|
||||
}
|
||||
|
||||
if (!($tusData = $this->cache->get($uploadKey))) {
|
||||
return $this->response(null, Response::HTTP_GONE);
|
||||
}
|
||||
|
||||
return $this->response(null, Response::HTTP_NO_CONTENT, [
|
||||
'Content-Type' => self::HEADER_CONTENT_TYPE,
|
||||
'Upload-Expires' => $tusData['expires_at'],
|
||||
'Upload-Offset' => $newOffset,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function verifyPatchRequest(array $meta): int
|
||||
{
|
||||
$uploadOffset = request()->header('upload-offset');
|
||||
|
||||
if ($uploadOffset && $uploadOffset !== (string) $meta['offset']) {
|
||||
return Response::HTTP_CONFLICT;
|
||||
}
|
||||
|
||||
$contentType = request()->header('Content-Type');
|
||||
|
||||
if ($contentType !== self::HEADER_CONTENT_TYPE) {
|
||||
return Response::HTTP_UNSUPPORTED_MEDIA_TYPE;
|
||||
}
|
||||
|
||||
return Response::HTTP_OK;
|
||||
}
|
||||
|
||||
protected function handleDelete(): Response
|
||||
{
|
||||
$uploadKey = $this->getUploadKeyFromUrl();
|
||||
$tusData = $this->cache->get($uploadKey);
|
||||
$resource = $tusData['file_path'] ?? null;
|
||||
|
||||
if (!$resource) {
|
||||
return $this->response(null, Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$isDeleted = $this->cache->delete($uploadKey);
|
||||
|
||||
if (!$isDeleted || !file_exists($resource)) {
|
||||
return $this->response(null, Response::HTTP_GONE);
|
||||
}
|
||||
|
||||
unlink($resource);
|
||||
|
||||
return $this->response(null, Response::HTTP_NO_CONTENT, [
|
||||
'Tus-Extension' => 'termination',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getClientChecksum(): string
|
||||
{
|
||||
$checksumHeader = request()->header('Upload-Checksum');
|
||||
|
||||
if (empty($checksumHeader)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
[$checksumAlgorithm, $checksum] = explode(' ', $checksumHeader);
|
||||
|
||||
$checksum = base64_decode($checksum);
|
||||
|
||||
if (
|
||||
$checksum === false ||
|
||||
!in_array($checksumAlgorithm, hash_algos(), true)
|
||||
) {
|
||||
abort(Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $checksum;
|
||||
}
|
||||
|
||||
protected function verifyChecksum(string $checksum, string $filePath): bool
|
||||
{
|
||||
if (empty($checksum)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $checksum ===
|
||||
hash_file($this->getChecksumAlgorithm(), $filePath);
|
||||
}
|
||||
|
||||
protected function getChecksumAlgorithm(): ?string
|
||||
{
|
||||
$checksumHeader = request()->header('Upload-Checksum');
|
||||
|
||||
if (empty($checksumHeader)) {
|
||||
return self::DEFAULT_CHECKSUM_ALGORITHM;
|
||||
}
|
||||
|
||||
[$checksumAlgorithm] = explode(' ', $checksumHeader);
|
||||
|
||||
return $checksumAlgorithm;
|
||||
}
|
||||
|
||||
protected function getOrCreateUploadKey(): string
|
||||
{
|
||||
if (!empty($this->uploadKey)) {
|
||||
return $this->uploadKey;
|
||||
}
|
||||
|
||||
$key = request()->header('Upload-Key') ?? Uuid::uuid4()->toString();
|
||||
|
||||
if (empty($key)) {
|
||||
abort(Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$this->uploadKey = $key;
|
||||
|
||||
return $this->uploadKey;
|
||||
}
|
||||
|
||||
protected function extractMeta(): array
|
||||
{
|
||||
$uploadMetaData = request()->header('Upload-Metadata');
|
||||
|
||||
if (empty($uploadMetaData)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$uploadMetaDataChunks = explode(',', $uploadMetaData);
|
||||
|
||||
$result = [];
|
||||
foreach ($uploadMetaDataChunks as $chunk) {
|
||||
$pieces = explode(' ', trim($chunk));
|
||||
|
||||
$key = $pieces[0];
|
||||
$value = $pieces[1] ?? '';
|
||||
|
||||
$result[$key] = base64_decode($value);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function getUploadKeyFromUrl(): string
|
||||
{
|
||||
return basename(request()->getPathInfo());
|
||||
}
|
||||
|
||||
protected function response(
|
||||
string $content = null,
|
||||
int $status = 200,
|
||||
array $headers = [],
|
||||
): Response {
|
||||
$mergedHeaders = array_merge(
|
||||
[
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'Tus-Resumable' => self::TUS_PROTOCOL_VERSION,
|
||||
'Access-Control-Allow-Origin' => request()->header('Origin'),
|
||||
'Access-Control-Allow-Methods' => implode(
|
||||
',',
|
||||
$this->allowedHttpVerbs,
|
||||
),
|
||||
'Access-Control-Allow-Headers' =>
|
||||
'Origin, X-Requested-With, Content-Type, Content-Length, Upload-Key, Upload-Checksum, Upload-Length, Upload-Offset, Tus-Version, Tus-Resumable, Upload-Metadata',
|
||||
'Access-Control-Expose-Headers' =>
|
||||
'Upload-Key, Upload-Checksum, Upload-Length, Upload-Offset, Upload-Metadata, Tus-Version, Tus-Resumable, Tus-Extension, Location',
|
||||
'Access-Control-Max-Age' => 86400, // 24 hours
|
||||
],
|
||||
$headers,
|
||||
);
|
||||
|
||||
return response($content, $status, $mergedHeaders);
|
||||
}
|
||||
}
|
||||
30
common/Files/Tus/TusServiceProvider.php
Executable file
30
common/Files/Tus/TusServiceProvider.php
Executable file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Files\Tus;
|
||||
|
||||
use Common\Core\AppUrl;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use TusPhp\Tus\Server as TusServer;
|
||||
|
||||
class TusServiceProvider extends ServiceProvider
|
||||
{
|
||||
static function uploadDir(): string
|
||||
{
|
||||
return storage_path('tus');
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton('tus-server', function () {
|
||||
$server = new TusServer(config('cache.default'));
|
||||
|
||||
$baseUri = app(AppUrl::class)->htmlBaseUri;
|
||||
|
||||
$server
|
||||
->setApiPath("{$baseUri}api/v1/tus/upload")
|
||||
->setUploadDir(TusServiceProvider::uploadDir());
|
||||
|
||||
return $server;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user