Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2522337
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
9 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/app/Backends/Storage.php b/src/app/Backends/Storage.php
index 47f57e1a..346e58e9 100644
--- a/src/app/Backends/Storage.php
+++ b/src/app/Backends/Storage.php
@@ -1,294 +1,293 @@
<?php
namespace App\Backends;
use App\Fs\Chunk;
use App\Fs\Item;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage as LaravelStorage;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\StreamedResponse;
class Storage
{
/** @const How long the resumable upload "token" is valid (in seconds) */
public const UPLOAD_TTL = 60 * 60 * 6;
/**
* Delete a file.
*
* @param \App\Fs\Item $file File object
*
* @throws \Exception
*/
public static function fileDelete(Item $file): void
{
$disk = LaravelStorage::disk('files');
$path = $file->path . '/' . $file->id;
// TODO: Deleting files might be slow, consider marking as deleted and async job
$disk->deleteDirectory($path);
$file->forceDelete();
}
/**
* Delete a file chunk.
*
* @param \App\Fs\Chunk $chunk File chunk object
*
* @throws \Exception
*/
public static function fileChunkDelete(Chunk $chunk): void
{
$disk = LaravelStorage::disk('files');
$path = self::chunkLocation($chunk->chunk_id, $chunk->item);
$disk->delete($path);
$chunk->forceDelete();
}
/**
* File download handler.
*
* @param \App\Fs\Item $file File object
*
* @throws \Exception
*/
public static function fileDownload(Item $file): StreamedResponse
{
$response = new StreamedResponse();
$props = $file->getProperties(['name', 'size', 'mimetype']);
// Prepare the file name for the Content-Disposition header
$extension = pathinfo($props['name'], \PATHINFO_EXTENSION) ?: 'file';
$fallbackName = str_replace('%', '', Str::ascii($props['name'])) ?: "file.{$extension}";
$disposition = $response->headers->makeDisposition('attachment', $props['name'], $fallbackName);
$response->headers->replace([
- 'Content-Length' => $props['size'] ?: 0,
'Content-Type' => $props['mimetype'],
'Content-Disposition' => $disposition,
]);
$response->setCallback(function () use ($file) {
$file->chunks()->orderBy('sequence')->get()->each(function ($chunk) use ($file) {
$disk = LaravelStorage::disk('files');
$path = Storage::chunkLocation($chunk->chunk_id, $file);
$stream = $disk->readStream($path);
fpassthru($stream);
fclose($stream);
});
});
return $response;
}
/**
* File upload handler
*
* @param resource $stream File input stream
* @param array $params Request parameters
* @param ?\App\Fs\Item $file The file object
*
* @return array File/Response attributes
* @throws \Exception
*/
public static function fileInput($stream, array $params, Item $file = null): array
{
if (!empty($params['uploadId'])) {
return self::fileInputResumable($stream, $params, $file);
}
$disk = LaravelStorage::disk('files');
$chunkId = \App\Utils::uuidStr();
$path = self::chunkLocation($chunkId, $file);
$disk->writeStream($path, $stream);
$fileSize = $disk->size($path);
if ($file->type & Item::TYPE_INCOMPLETE) {
$file->type -= Item::TYPE_INCOMPLETE;
$file->save();
}
// Update the file type and size information
$file->setProperties([
'size' => $fileSize,
'mimetype' => self::mimetype($path),
]);
// Assign the node to the file, "unlink" any old nodes of this file
$file->chunks()->delete();
$file->chunks()->create([
'chunk_id' => $chunkId,
'sequence' => 0,
'size' => $fileSize,
]);
return ['id' => $file->id];
}
/**
* Resumable file upload handler
*
* @param resource $stream File input stream
* @param array $params Request parameters
* @param ?\App\Fs\Item $file The file object
*
* @return array File/Response attributes
* @throws \Exception
*/
protected static function fileInputResumable($stream, array $params, Item $file = null): array
{
// Initial request, save file metadata, return uploadId
if ($params['uploadId'] == 'resumable') {
if (empty($params['size']) || empty($file)) {
throw new \Exception("Missing parameters of resumable file upload.");
}
$params['uploadId'] = \App\Utils::uuidStr();
$upload = [
'fileId' => $file->id,
'size' => $params['size'],
'uploaded' => 0,
];
if (!Cache::add('upload:' . $params['uploadId'], $upload, self::UPLOAD_TTL)) {
throw new \Exception("Failed to create cache entry for resumable file upload.");
}
return [
'uploadId' => $params['uploadId'],
'uploaded' => 0,
'maxChunkSize' => (\config('octane.swoole.options.package_max_length') ?: 10 * 1024 * 1024) - 8192,
];
}
$upload = Cache::get('upload:' . $params['uploadId']);
if (empty($upload)) {
throw new \Exception("Cache entry for resumable file upload does not exist.");
}
$file = Item::find($upload['fileId']);
if (!$file) {
throw new \Exception("Invalid fileId for resumable file upload.");
}
$from = $params['from'] ?? 0;
// Sanity checks on the input parameters
// TODO: Support uploading again a chunk that already has been uploaded?
if ($from < $upload['uploaded'] || $from > $upload['uploaded'] || $from > $upload['size']) {
throw new \Exception("Invalid 'from' parameter for resumable file upload.");
}
$disk = LaravelStorage::disk('files');
$chunkId = \App\Utils::uuidStr();
$path = self::chunkLocation($chunkId, $file);
// Save the file chunk
$disk->writeStream($path, $stream);
// Detect file type using the first chunk
if ($from == 0) {
$upload['mimetype'] = self::mimetype($path);
$upload['chunks'] = [];
}
$chunkSize = $disk->size($path);
// Create the chunk record
$file->chunks()->create([
'chunk_id' => $chunkId,
'sequence' => count($upload['chunks']),
'size' => $chunkSize,
'deleted_at' => \now(), // not yet active chunk
]);
$upload['chunks'][] = $chunkId;
$upload['uploaded'] += $chunkSize;
// Update the file metadata after the upload of all chunks is completed
if ($upload['uploaded'] >= $upload['size']) {
if ($file->type & Item::TYPE_INCOMPLETE) {
$file->type -= Item::TYPE_INCOMPLETE;
$file->save();
}
// Update file metadata
$file->setProperties([
'size' => $upload['uploaded'],
'mimetype' => $upload['mimetype'] ?: 'application/octet-stream',
]);
// Assign uploaded chunks to the file, "unlink" any old chunks of this file
$file->chunks()->delete();
$file->chunks()->whereIn('chunk_id', $upload['chunks'])->restore();
// TODO: Create a "cron" job to remove orphaned nodes from DB and the storage.
// I.e. all with deleted_at set and older than UPLOAD_TTL
// Delete the upload cache record
Cache::forget('upload:' . $params['uploadId']);
return ['id' => $file->id];
}
// Update the upload metadata
Cache::put('upload:' . $params['uploadId'], $upload, self::UPLOAD_TTL);
return ['uploadId' => $params['uploadId'], 'uploaded' => $upload['uploaded']];
}
/**
* Get the file mime type.
*
* @param string $path File location
*
* @return string File mime type
*/
protected static function mimetype(string $path): string
{
$disk = LaravelStorage::disk('files');
$mimetype = $disk->mimeType($path);
// The mimetype may contain e.g. "; charset=UTF-8", remove this
if ($mimetype) {
return explode(';', $mimetype)[0];
}
return 'application/octet-stream';
}
/**
* Node location in the storage
*
* @param string $chunkId Chunk identifier
* @param \App\Fs\Item $file File the chunk belongs to
*
* @return string Chunk location
*/
public static function chunkLocation(string $chunkId, Item $file): string
{
return $file->path . '/' . $file->id . '/' . $chunkId;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Jan 10, 5:01 PM (22 h, 12 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
421929
Default Alt Text
(9 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment