Add file manager functionality

This commit is contained in:
Patrik Leifert 2024-03-01 02:26:41 +01:00
parent 679a132407
commit 8acf308554
10 changed files with 306 additions and 0 deletions

1
dist/.gitignore vendored
View File

@ -20,3 +20,4 @@ yarn-error.log
/.vscode
_ide_helper.php
_ide_helper_models.php
/public/game-files

View File

@ -3,6 +3,8 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\GameFile;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
@ -38,4 +40,9 @@ public function getBattleyePatch()
return response()->download($disk->path('bottleEye/BEClient_x64.dll'));
}
public function getGameFileList() : JsonResponse
{
return response()->json(GameFile::select(['name', 'hash', 'game_path'])->latest()->get(), 200);
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use App\Models\GameFile;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
class GameFileController extends Controller
{
public function store(Request $request)
{
if (!auth()->user()->can(\App\Enums\Auth\Permissions::FILE_UPLOAD->value)) {
throw new AuthorizationException();
}
// Validate the incoming request with appropriate rules for file uploads
$request->validate([
'files.*' => 'required|file',
'gamepath.*' => 'required|string',
]);
//dd($request);
$duplicateFiles = [];
$overwrittenFiles = [];
$uploadedFiles = [];
// Handle file upload logic
if ($request->hasFile('files')) {
for($i = 0; $i < count($request->file('files')); $i++) {
$file = $request->file('files')[$i];
$filename = $file->getClientOriginalName();
$filehash = str(hash_file('sha256', $file->getRealPath()))->upper();
$gameFile = GameFile::where('name', $filename)->first() ?? new GameFile;
if ($gameFile->hash == $filehash) {
$duplicateFiles[] = $gameFile->name;
continue;
}
if (isset($gameFile->id)) {
$overwrittenFiles[] = $gameFile->name;
}
$gameFile->name = $filename;
$gameFile->hash = $filehash;
$file->storeAs('', $gameFile->name, ['disk' => 'dg_public']);
$uploadedFiles[] = $gameFile->name;
$gameFile->game_path = $request->game_path[$i];
$gameFile->save();
}
}
if (count($uploadedFiles) === 0) {
Session::flash('alert-error', 'No files were uploaded');
}
// Optionally, you can return a response
if (count($overwrittenFiles) > 0) {
Session::flash('alert-warning', 'Files uploaded successfully. The following files was overwritten: <br>' . implode('<br>', $overwrittenFiles));
//return response()->json(['message' => 'Only some files was uploaded successfully. The following files already exist: ' . implode(', ', $overwrittenFiles)]);
}
if (count($duplicateFiles) > 0) {
Session::flash('alert-warning', 'The following files already exist: <br>' . implode('<br>', $duplicateFiles));
//return response()->json(['message' => 'Only some files was uploaded successfully. The following files already exist: ' . implode(', ', $duplicateFiles)]);
}
if (count($uploadedFiles) > 0) {
Session::flash('alert-success', 'Files uploaded successfully <br>' . implode('<br>', $uploadedFiles));
//return response()->json(['message' => 'Files uploaded successfully']);
}
return redirect()->back();
//return response()->json(['message' => 'Files uploaded successfully']);
}
public function index() : View {
if (!auth()->user()->can(\App\Enums\Auth\Permissions::FILE_UPLOAD->value)) {
throw new AuthorizationException();
}
return view('file-manager', ['files' => GameFile::latest()->get()]);
}
}

11
dist/app/Models/GameFile.php vendored Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class GameFile extends Model
{
use HasFactory;
}

View File

@ -44,6 +44,11 @@
'throw' => false,
],
'dg_public' => [
'driver' => 'local',
'root' => public_path().'/game-files',
],
'patches' => [
'driver' => 'local',
'root' => resource_path('patches'),

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('game_files', function (Blueprint $table) {
$table->id();
$table->string('name', 128);
$table->string('game_path');
$table->string('hash', 64); // SHA-256 hash is 64 characters long
$table->integer('version')->default(1); // Default version is 1
$table->tinyInteger('action')->default(1); //add = 1, delete = 2
$table->timestamps();
$table->unique(['name', 'hash', 'version']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('game_files');
}
};

View File

@ -0,0 +1,5 @@
<input type="file"
{{ $attributes->merge(['class' =>
"block w-full text-sm text-slate-500 file:mr-4 file:py-2.5 file:px-4 file:rounded-md file:border-0 file:border-gray-600 file:font-semibold file:cursor-pointer file:bg-gray-800/75 file:text-gray-300 hover:file:bg-gray-900 hover:file:text-white"
])}}
/>

View File

@ -0,0 +1,143 @@
<x-raw-layout>
<!-- Nothing in life is to be feared, it is only to be understood. Now is the time to understand more, so that we may fear less. - Marie Curie -->
<h1 class="text-4xl font-semilight p-10">Deathgarden file manager</h1>
@if(Session::has('alert-error'))
<x-alerts.error heading="An error occured">{!! Session::get('alert-error') !!}</x-alerts.error>
@endif
@if(Session::has('alert-success'))
<x-alerts.success heading="Success">{!! Session::get('alert-success') !!}</x-alerts.success>
@endif
@if(Session::has('alert-warning'))
<x-alerts.warning heading="Warning">{!! Session::get('alert-warning') !!}</x-alerts.warning>
@endif
<div class="flex flex-col items-center justify-center p-12">
<div class="mx-auto w-full max-w-7xl">
<x-tables.table>
<x-tables.thead>
<x-tables.th>Name</x-tables.th>
<x-tables.th>Hash</x-tables.th>
<x-tables.th>Size</x-tables.th>
<x-tables.th>Last update</x-tables.th>
<x-tables.th>Actions</x-tables.th>
</x-tables.thead>
<tbody>
@foreach($files as $file)
<tr class="text-center">
<x-tables.td>
{{ $file->name }}
</x-tables.td>
<x-tables.td>
{{ $file->hash }}
</x-tables.td>
<x-tables.td>
@if(Storage::disk('dg_public')->exists($file->name))
{{ Storage::disk('dg_public')->size($file->name) / 1000 }} kB
@else
Error while fetching file
@endif
</x-tables.td>
<x-tables.td>
{{ $file->updated_at }}
</x-tables.td>
<x-tables.td>
<!-- <a href="">Delete</a> -->
</x-tables.td>
</tr>
@endforeach
</tbody>
</x-tables.table>
</div>
<div class="mx-auto w-full max-w-7xl mt-8">
<form action="{{ route('file.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<div id="fileInputsContainer">
<div class="flex flex-wrap gap-4">
<div class="w-3/5 flex items-center mb-2">
<x-inputs.text-input name="game_path[]" />
</div>
<div class="flex items-center mb-2">
<x-inputs.file-input name="files[]" />
</div>
</div>
</div>
<div class="flex justify-end mt-4">
<div class="w-auto mx-2">
<button type="button" id="addFileInput" class="inline-flex rounded-md bg-gray-800/75 px-6 py-2 font-semibold text-gray-300 hover:text-white transition focus:outline-none focus-visible:ring-2 focus-visible:ring-[#BD92F5] focus-visible:ring-offset-2 focus-visible:ring-offset-black">
Add More Files
</button>
</div>
<div class="w-auto mx-2">
<button class="inline-flex rounded-md bg-gray-800/75 px-6 py-2 font-semibold text-gray-300 hover:text-white transition focus:outline-none focus-visible:ring-2 focus-visible:ring-[#BD92F5] focus-visible:ring-offset-2 focus-visible:ring-offset-black">
Submit
</button>
</div>
</div>
</form>
</div>
</div>
<script>
function addFileInputEventListener(fileInput) {
fileInput.addEventListener('change', function(event) {
var selectedFile = event.target.files[0];
var textInput = event.target.closest('.flex-wrap').querySelector('input[name="game_path[]"]');
if (textInput) {
if (selectedFile) {
var fileName = selectedFile.name;
fileName = examineFilePaths(fileName);
textInput.value = fileName;
} else {
textInput.value = '';
}
}
});
}
function examineFilePaths(fileName) {
switch (fileName) {
case 'TheExit_BE.exe':
return './TheExit/Binaries/Win64/' + fileName;
case 'BEClient_x64.dll':
return './TheExit/Binaries/Win64/BattlEye/' + fileName;
default:
break;
}
var extension = fileName.split('.').pop();
switch (extension) {
case 'pak':
return './TheExit/Content/Paks/' + fileName;
case 'sig':
return './TheExit/Content/Paks/' + fileName;
default:
return './' + fileName;
}
}
document.getElementById('addFileInput').addEventListener('click', function() {
var container = document.getElementById('fileInputsContainer');
var newInput = document.createElement('div');
newInput.classList.add('flex', 'flex-wrap', 'gap-4');
newInput.innerHTML = `
<div class="w-3/5 flex items-center mb-2">
<x-inputs.text-input name="game_path[]" />
</div>
<div class="flex items-center mb-2">
<x-inputs.file-input name="files[]" />
</div>
`;
container.appendChild(newInput);
// Add event listener to file input of the newly created input group
var fileInput = newInput.querySelector('input[name="files[]"]');
addFileInputEventListener(fileInput);
});
// Add event listener to file input of the initial input group
var initialFileInput = document.querySelector('input[name="files[]"]');
addFileInputEventListener(initialFileInput);
</script>
</x-raw-layout>

2
dist/routes/api.php vendored
View File

@ -19,6 +19,8 @@
Route::get('patch/battleye', [PatchController::class, 'getBattleyePatch']);
});
Route::get('patch/files', [PatchController::class, 'getGameFileList']);
Route::fallback(function () {
return response('route not found', 404);
});

5
dist/routes/web.php vendored
View File

@ -32,6 +32,11 @@
return \Inertia\Inertia::render('Dashboard');
});
Route::middleware('auth')->group(function () {
Route::get('/admin/file-manager', [\App\Http\Controllers\Web\GameFileController::class, 'index']);
Route::post('/admin/file-manager', [\App\Http\Controllers\Web\GameFileController::class, 'store'])->name('file.store');
});
Route::middleware('verify_migration_key')->get('/migrate-database', function () {
Artisan::call('migrate --no-interaction');
print Artisan::output();