Add fole check endpoint & Update dependencies

This commit is contained in:
2026-08-05 22:01:32 +02:00
parent 0702d445b2
commit b8f7bc48e1
10 changed files with 2378 additions and 1506 deletions
+40 -8
View File
@@ -9,7 +9,6 @@ use Illuminate\Support\Facades\Storage;
class ApiController extends Controller
{
/**
* Get Filesize of File
*/
@@ -19,7 +18,7 @@ class ApiController extends Controller
// Decrypt Hash
$file = Crypt::decryptString($folderhash);
$time = Crypt::decryptString($timehash);
} catch (DecryptException $e) {
} catch (DecryptException $e) {
return response()->json([
'error' => 'Invalid Token!',
], 500);
@@ -32,7 +31,7 @@ class ApiController extends Controller
], 404);
}
if (!Storage::exists($file)) {
if (! Storage::exists($file)) {
return response()->json([
'error' => 'File not found!',
], 404);
@@ -44,7 +43,39 @@ class ApiController extends Controller
}
/**
* Download File
* Check if a storage path exists
*/
public function checkPath(string $folderhash, string $timehash): \Illuminate\Http\JsonResponse
{
try {
$path = Crypt::decryptString($folderhash);
$time = Crypt::decryptString($timehash);
} catch (DecryptException $e) {
return response()->json(['valid' => false, 'error' => 'Invalid Token!'], 422);
}
if (Carbon::now() > Carbon::parse($time)) {
return response()->json(['valid' => false, 'error' => 'Link has expired!'], 410);
}
// Defensive: reject traversal; only allow the known prefixes / year dirs.
if (str_contains($path, '..')) {
return response()->json(['valid' => false, 'error' => 'Invalid path!'], 422);
}
if (! Storage::exists($path)) {
return response()->json(['valid' => false, 'error' => 'File not found!'], 404);
}
return response()->json([
'valid' => true,
'type' => Storage::directoryExists($path) ? 'directory' : 'file',
'size' => Storage::directoryExists($path) ? null : Storage::size($path),
]);
}
/**
* Download File
*/
public function download(string $folderhash, string $timehash)
{
@@ -63,26 +94,27 @@ class ApiController extends Controller
}
$filename = explode('/', $file);
$filePath = storage_path('app/' . $file);
$filePath = storage_path('app/'.$file);
$headers = [
'Content-Type' => Storage::mimeType($file),
'Content-Length' => Storage::size($file),
'Content-Disposition' => 'attachment; filename="' . end($filename) . '"',
'Content-Disposition' => 'attachment; filename="'.end($filename).'"',
];
// Stream the file in chunks to prevent memory issues
return response()->stream(
function () use ($filePath) {
$fd = fopen($filePath, 'rb');
while (!feof($fd)) {
while (! feof($fd)) {
echo fread($fd, 2048);
}
},
200,
$headers
);
} catch (DecryptException $e) { }
} catch (DecryptException $e) {
}
return view('error', ['error' => 'Invalid Token!']);
}
+2 -2
View File
@@ -29,7 +29,7 @@ class Kernel extends HttpKernel
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => []
'web' => [],
];
/**
@@ -40,6 +40,6 @@ class Kernel extends HttpKernel
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];
}
+9 -9
View File
@@ -5,19 +5,19 @@
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.1",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8"
"php": "^8.2",
"guzzlehttp/guzzle": "^7.8.1",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.2",
"laravel/tinker": "^2.10"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
"laravel/pint": "^1.0",
"fakerphp/faker": "^1.24.0",
"laravel/pint": "^1.18",
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.1",
"nunomaduro/collision": "^8.1",
"phpunit/phpunit": "^11.4",
"spatie/laravel-ignition": "^2.0"
},
"autoload": {
Generated
+2168 -1487
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
<?php
return [
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
],
'password_timeout' => 10800,
];
+2
View File
@@ -16,3 +16,5 @@ use Illuminate\Support\Facades\Route;
Route::get('/download/{folderhash}/{timehash}', [ApiController::class, 'download']);
Route::get('/getSize/{folderhash}/{timehash}', [ApiController::class, 'getFileSize']);
Route::get('/check/{folderhash}/{timehash}', [ApiController::class, 'checkPath'])
->middleware('throttle:120,1');
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Tests;
use Illuminate\Contracts\Console\Kernel;
trait CreatesApplication
{
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
return $app;
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class CheckPathTest extends TestCase
{
private function tokenize(string $value): string
{
return Crypt::encryptString($value);
}
public function test_valid_file_returns_size(): void
{
Storage::fake('local');
Storage::put('hentai-1080p/2026/Title/E01.mp4', 'fake-content');
$folder = $this->tokenize('hentai-1080p/2026/Title/E01.mp4');
$time = $this->tokenize(now()->addHours(6)->toDateTimeString());
$this->getJson("/check/{$folder}/{$time}")
->assertOk()
->assertJson([
'valid' => true,
'type' => 'file',
])
->assertJsonPath('size', strlen('fake-content'));
}
public function test_valid_directory_is_recognized(): void
{
Storage::fake('local');
Storage::put('2026/Title/E01/720/manifest.mpd', 'fake-manifest');
$folder = $this->tokenize('2026/Title');
$time = $this->tokenize(now()->addHours(6)->toDateTimeString());
$this->getJson("/check/{$folder}/{$time}")
->assertOk()
->assertJson([
'valid' => true,
'type' => 'directory',
'size' => null,
]);
}
public function test_missing_path_returns_404(): void
{
Storage::fake('local');
$folder = $this->tokenize('hentai/2026/Title/E01.mp4');
$time = $this->tokenize(now()->addHours(6)->toDateTimeString());
$this->getJson("/check/{$folder}/{$time}")
->assertStatus(404)
->assertJson(['valid' => false]);
}
public function test_expired_token_returns_410(): void
{
Storage::fake('local');
Storage::put('hentai/2026/Title/E01.mp4', 'fake-content');
$folder = $this->tokenize('hentai/2026/Title/E01.mp4');
$time = $this->tokenize(now()->subMinute()->toDateTimeString());
$this->getJson("/check/{$folder}/{$time}")
->assertStatus(410)
->assertJson(['valid' => false]);
}
public function test_invalid_token_returns_422(): void
{
Storage::fake('local');
$this->getJson('/check/not-a-token/not-a-token')
->assertStatus(422)
->assertJson(['valid' => false]);
}
public function test_path_traversal_is_rejected(): void
{
Storage::fake('local');
$folder = $this->tokenize('../../etc/passwd');
$time = $this->tokenize(now()->addHours(6)->toDateTimeString());
$this->getJson("/check/{$folder}/{$time}")
->assertStatus(422)
->assertJson(['valid' => false]);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
}
View File