FEATURE #1337: add file upload
This commit registers new items into admin menu and adds functionality for uploading files.
This commit is contained in:
+2
-1
@@ -1,3 +1,5 @@
|
||||
/public/uploads/*
|
||||
!/public/uploads/.gitkeep
|
||||
/resources/views/vendor
|
||||
!/resources/views/vendor/.gitkeep
|
||||
/.env
|
||||
@@ -6,4 +8,3 @@
|
||||
/node_modules
|
||||
Homestead.yaml
|
||||
Homestead.json
|
||||
.env
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class UploadFile extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
/**
|
||||
* @var UploadedFile
|
||||
*/
|
||||
private $uploadedFile;
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param File $uploadedFile
|
||||
* @param $id
|
||||
*/
|
||||
public function __construct(File $uploadedFile, $id)
|
||||
{
|
||||
$this->uploadedFile = $uploadedFile;
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should be broadcast on.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Http\Requests\UploadImageRequest;
|
||||
use App\Models\File;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
|
||||
class UploadFile extends Controller
|
||||
{
|
||||
/**
|
||||
* @var File
|
||||
*/
|
||||
private $file;
|
||||
|
||||
function __construct(File $file)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->file = $file;
|
||||
}
|
||||
|
||||
public function getFile()
|
||||
{
|
||||
return view('admin/fileManager.newFile');
|
||||
}
|
||||
|
||||
public function getImage()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads any file
|
||||
*
|
||||
* @param UploadFileRequest $request
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function postFile(UploadFileRequest $request)
|
||||
{
|
||||
if (!$request->hasFile('file')) {
|
||||
return $this->invalidUpload('noFileSpecified');
|
||||
}
|
||||
|
||||
try {
|
||||
$uploadedFile = $this->file->upload($request->file('file'), $request);
|
||||
|
||||
// we do not want to redirect
|
||||
if ($request->ajax()) {
|
||||
return $this->jsonResponse($uploadedFile);
|
||||
}
|
||||
|
||||
// upload was not performed via ajax, redirect user
|
||||
return redirect()
|
||||
->route('upload.file')
|
||||
->with('success', trans('admin/fileManager.uploadSuccessfull'));
|
||||
} catch (FileException $e) {
|
||||
return $this->invalidUpload('invalidFile');
|
||||
}
|
||||
}
|
||||
|
||||
protected function invalidUpload($message)
|
||||
{
|
||||
return redirect()
|
||||
->route('upload.file')
|
||||
->with('error', trans('admin/fileManager.' . $message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads only images
|
||||
*
|
||||
* @param UploadImageRequest $request
|
||||
*/
|
||||
public function postImage(UploadImageRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Send response about uploaded file as JSON
|
||||
*
|
||||
* @param $uploadedFile
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
private function jsonResponse($uploadedFile)
|
||||
{
|
||||
$filename = $uploadedFile['file']->getFilename();
|
||||
$url = config('app.upload_path') . DIRECTORY_SEPARATOR . $filename;
|
||||
|
||||
$data = [
|
||||
'filename' => $filename,
|
||||
'url' => $url,
|
||||
'id' => $uploadedFile['id']
|
||||
];
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Http\Requests\UploadImageRequest;
|
||||
|
||||
class Uploader extends Controller
|
||||
{
|
||||
/**
|
||||
* Uploads any file
|
||||
*
|
||||
* @param UploadFileRequest $request
|
||||
*/
|
||||
public function postFile(UploadFileRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads only images
|
||||
*
|
||||
* @param UploadImageRequest $request
|
||||
*/
|
||||
public function postImage(UploadImageRequest $request)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,13 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
class UploadFileRequest extends Request
|
||||
class UploadFileRequest extends UploadRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
* @internal param array $rules
|
||||
*/
|
||||
public function rules()
|
||||
protected function setRules()
|
||||
{
|
||||
return [
|
||||
'file' => 'required'
|
||||
];
|
||||
$this->rules = ['file' => 'required|image'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
abstract class UploadRequest extends Request
|
||||
{
|
||||
protected $rules = [];
|
||||
|
||||
abstract protected function setRules();
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
// some globaly defined rules
|
||||
$rules = [
|
||||
'keywords' => 'max:255'
|
||||
];
|
||||
|
||||
// setup additional rules
|
||||
$this->setRules();
|
||||
|
||||
foreach ($this->rules as $field => $rule) {
|
||||
$rules[$field] = $rule;
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -90,12 +90,16 @@ Route::group([
|
||||
->name('history.diff');
|
||||
|
||||
// uploader methods
|
||||
Route::post('upload/file', 'Uploader@postFile')
|
||||
Route::get('upload/file', 'UploadFile@getFile')
|
||||
->name('upload.file');
|
||||
|
||||
Route::post('upload/image', 'Uploader@postImage')
|
||||
->name('upload.image');
|
||||
|
||||
// uploader methods
|
||||
Route::post('upload/file', 'UploadFile@postFile')
|
||||
->name('upload.postFile');
|
||||
|
||||
// custom fields
|
||||
// ------------------------------
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\Uploadfile;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class LogUploadFile
|
||||
{
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param Uploadfile $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(Uploadfile $event)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
+103
-6
@@ -2,24 +2,121 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Events\UploadFile;
|
||||
use App\Http\Requests\UploadRequest;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Symfony\Component\HttpFoundation\File\File as SymphonyFile;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
/**
|
||||
* Class File
|
||||
*/
|
||||
class File extends Model
|
||||
{
|
||||
|
||||
/**
|
||||
* Hash algorithm used for hashing file
|
||||
*/
|
||||
const HASH_ALGORITHM = 'sha256';
|
||||
/**
|
||||
* @var bool $timstamps have created_at and updated_at fields
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'filename',
|
||||
'sha256_hash',
|
||||
'mime_type'
|
||||
'original_filename',
|
||||
'file_hash',
|
||||
'hash_algorithm',
|
||||
'mime_type',
|
||||
'description',
|
||||
'keywords',
|
||||
'directory'
|
||||
];
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
/**
|
||||
* Getter for filename of uploaded file.
|
||||
*
|
||||
* This filename is used for localy stored file.
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFilename(UploadedFile $file)
|
||||
{
|
||||
return uniqid() . '.' . $file->getClientOriginalExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves file into FS and creates new record in database
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
* @param UploadRequest $request
|
||||
*
|
||||
* @return array associative array - key 'file' is type of SymphonyFile and contains info about real file, key 'id'
|
||||
* is ID of row in files table
|
||||
*/
|
||||
public function upload(UploadedFile $file, UploadRequest $request)
|
||||
{
|
||||
$destination = config('app.upload_path');
|
||||
$filename = $this->getFilename($file);
|
||||
|
||||
// save file into physical location (local FS)
|
||||
$uploadedFile = $file->move($destination, $filename);
|
||||
|
||||
// insert record into database
|
||||
$id = $this->saveIntoDatabase($uploadedFile, $request, $file->getClientOriginalName());
|
||||
|
||||
// fire event about new file
|
||||
Event::fire(new UploadFile($uploadedFile, $id));
|
||||
|
||||
return [
|
||||
'file' => $file,
|
||||
'id' => $id
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Save file into database
|
||||
*
|
||||
* This file saves new record into database. It also calculates hash of given file, obtains MIME type etc.
|
||||
*
|
||||
* @param SymphonyFile $file
|
||||
* @param UploadRequest $request
|
||||
* @param $originalFilename
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function saveIntoDatabase(SymphonyFile $file, UploadRequest $request, $originalFilename)
|
||||
{
|
||||
// get path and hash of uploaded file
|
||||
$path = config('app.upload_path') . DIRECTORY_SEPARATOR . $file->getFilename();
|
||||
$hash = hash_file(self::HASH_ALGORITHM, $path);
|
||||
|
||||
// define mandatory fields
|
||||
$params = [
|
||||
'user_id' => Auth::user()->id,
|
||||
'filename' => $file->getFilename(),
|
||||
'original_filename' => $originalFilename,
|
||||
'file_hash' => $hash,
|
||||
'hash_algorithm' => self::HASH_ALGORITHM,
|
||||
'mime_type' => $file->getMimeType(),
|
||||
];
|
||||
|
||||
// save optional fields
|
||||
$optionalFields = ['description', 'keywords', 'directory'];
|
||||
foreach ($optionalFields as $field) {
|
||||
if ($request->has($field)) {
|
||||
$params[$field] = $request->$field;
|
||||
}
|
||||
}
|
||||
|
||||
// create new database row
|
||||
$newFile = File::create($params);
|
||||
|
||||
return $newFile->id;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,9 @@ class AdminMenusProvider extends ServiceProvider
|
||||
$sub->route('users.overview', trans('admin/mainMenu.usersOverview'), [], 30, ['icon' => 'fa fa-users']);
|
||||
|
||||
}, 20, ['icon' => 'fa fa-users']);
|
||||
$menu->dropdown(trans('admin/mainMenu.fileManager'), function ($sub) {
|
||||
$sub->route('upload.file', trans('admin/mainMenu.uploadFile'), [], 10, ['icon' => 'fa fa-file']);
|
||||
}, 25, ['icon' => 'fa fa-film']);
|
||||
$menu->dropdown(trans('admin/mainMenu.customFields'), function ($sub) {
|
||||
$sub->route('customfields.new', trans('admin/mainMenu.customFieldsNew'), [], 10, ['icon' => 'fa fa-code']);
|
||||
$sub->route('customfields.overview', trans('admin/mainMenu.customFieldsOverview'), [], 50, ['icon' => 'fa fa-code-fork']);
|
||||
|
||||
@@ -31,6 +31,9 @@ class EventServiceProvider extends ServiceProvider
|
||||
'App\Events\ChangeSettings' => [
|
||||
'App\Listeners\LogChangeSettings',
|
||||
],
|
||||
'App\Events\UploadFile' => [
|
||||
'App\Listeners\LogUploadFile'
|
||||
],
|
||||
// internal Laravel events - should work as auth.login and auth.logout, however it doesn't...
|
||||
'Illuminate\Auth\Events\Login' => [
|
||||
'App\Listeners\LogLogin'
|
||||
@@ -43,13 +46,13 @@ class EventServiceProvider extends ServiceProvider
|
||||
/**
|
||||
* Register any other events for your application.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Events\Dispatcher $events
|
||||
* @param \Illuminate\Contracts\Events\Dispatcher $events
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot(DispatcherContract $events)
|
||||
{
|
||||
parent::boot($events);
|
||||
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,13 @@ return [
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
|
||||
/**
|
||||
* Upload path used for storing all uploaded files
|
||||
*/
|
||||
'upload_path' => env('UPLOAD_PATH', public_path('uploads')),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Logging Configuration
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddExtendedFileInfo extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('files', function (Blueprint $table) {
|
||||
$table->string('original_filename')
|
||||
->after('filename')->comment = 'Original filename of uploaded file';
|
||||
$table->text('description')
|
||||
->nullable();
|
||||
$table->string('keywords')
|
||||
->nullable()
|
||||
->index('keywords_idx');
|
||||
$table->string('directory')
|
||||
->default('/')
|
||||
->index('directory_idx')->comment = 'Not real directory on FS, only virtual one - used for better user orientation in large file collections';
|
||||
$table->index('filename');
|
||||
$table->string('filename', 128)
|
||||
->comment('Filename of localy stored file')
|
||||
->change();
|
||||
$table->timestamp('updated_at')
|
||||
->nullable()
|
||||
->after('created_at');
|
||||
$table->renameColumn('sha256_hash', 'file_hash');
|
||||
$table->string('hash_algorithm', '16')
|
||||
->after('sha256_hash');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('files', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'original_filename',
|
||||
'description',
|
||||
'keywords',
|
||||
'directory',
|
||||
'updated_at',
|
||||
'hash_algorithm'
|
||||
]);
|
||||
$table->dropIndex('files_filename_index');
|
||||
$table->renameColumn('file_hash', 'sha256_hash');
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
return [
|
||||
'upload' => 'Upload new file',
|
||||
'newFile' => 'New file',
|
||||
'invalidFile' => 'The file you submitted is somehow not valid file!',
|
||||
'noFileSpecified' => 'You have not specified any file for upload',
|
||||
'uploadSuccessfull' => 'Your file was successfully uploaded',
|
||||
'selectFile' => 'Select file for upload',
|
||||
'keywords' => 'Keywords (comma separated)',
|
||||
'description' => 'File description'
|
||||
];
|
||||
@@ -8,6 +8,10 @@ return [
|
||||
'userDetail' => 'Detail',
|
||||
'customFields' => 'Custom fields',
|
||||
'customFieldsOverview' => 'Overview',
|
||||
'customFieldsNew' => 'New field',
|
||||
'settings' => 'Settings'
|
||||
'customFieldsNew' => 'New field',
|
||||
'settings' => 'Settings',
|
||||
'fileManager' => 'File manager',
|
||||
'uploadFile' => 'New file',
|
||||
'uploadImage' => 'New image',
|
||||
'uploadedFiles' => 'Overview'
|
||||
];
|
||||
@@ -0,0 +1,14 @@
|
||||
@extends('admin.layout')
|
||||
|
||||
@section('pageName', @trans('admin/fileManager.newFile'))
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="row">
|
||||
<h2>@lang('admin/fileManager.newFile')</h2>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
@include('admin.fileManager.newFileForm')
|
||||
</div>
|
||||
@stop
|
||||
@@ -0,0 +1,56 @@
|
||||
@if (session('success'))
|
||||
<div data-alert class="callout success">
|
||||
<strong>{{ session('success') }}</strong>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (session('error'))
|
||||
<div data-alert class="callout alert">
|
||||
<strong>{{ session('error') }}</strong>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{route('upload.postFile')}}" enctype="multipart/form-data">
|
||||
{!! csrf_field() !!}
|
||||
|
||||
<div class="row">
|
||||
<div class="column large-4">
|
||||
<label>
|
||||
@lang('admin/fileManager.selectFile')
|
||||
|
||||
<input type="file" name="file" id="file" />
|
||||
|
||||
@if (isset($errors) && $errors->has('file'))
|
||||
<div class="callout alert">
|
||||
{{ $errors->first('file') }}
|
||||
</div>
|
||||
@endif
|
||||
</label>
|
||||
</div>
|
||||
<div class="column large-8">
|
||||
<label>
|
||||
@lang('admin/fileManager.keywords')
|
||||
|
||||
<input type="text" name="keywords" id="keywords" />
|
||||
|
||||
@if (isset($errors) && $errors->has('keywords'))
|
||||
<div class="callout alert">
|
||||
{{ $errors->first('keywords') }}
|
||||
</div>
|
||||
@endif
|
||||
</label>
|
||||
|
||||
<label>
|
||||
@lang('admin/fileManager.description')
|
||||
|
||||
<textarea rows="3" name="description" id="description"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row text-right">
|
||||
<div class="column">
|
||||
<button type="submit" class="button primary">@lang('admin/fileManager.upload')</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -4,22 +4,27 @@ class WritePermissionsTest extends TestCase
|
||||
{
|
||||
public function testStorageDir()
|
||||
{
|
||||
$this->writeToDir('test.txt');
|
||||
$this->writeToDir(storage_path('test.txt'));
|
||||
}
|
||||
|
||||
public function testLogsDir()
|
||||
{
|
||||
$this->writeToDir('logs/test.txt');
|
||||
$this->writeToDir(storage_path('logs/test.txt'));
|
||||
}
|
||||
|
||||
public function testFrameworkDir()
|
||||
{
|
||||
$this->writeToDir('framework/test.txt');
|
||||
$this->writeToDir(storage_path('framework/test.txt'));
|
||||
}
|
||||
|
||||
public function testBootstrapCacheDir()
|
||||
{
|
||||
$this->writeToDir('../bootstrap/cache/test.txt');
|
||||
$this->writeToDir(storage_path('../bootstrap/cache/test.txt'));
|
||||
}
|
||||
|
||||
public function testUploadDir()
|
||||
{
|
||||
$this->writeToDir(config('app.upload_path').'/test.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,11 +32,10 @@ class WritePermissionsTest extends TestCase
|
||||
*
|
||||
* @param $path
|
||||
*/
|
||||
protected function writeToDir($path)
|
||||
protected function writeToDir($file)
|
||||
{
|
||||
$file = storage_path($path);
|
||||
$content = uniqid();
|
||||
$bytesWritten = @file_put_contents($file, $content);
|
||||
$bytesWritten = file_put_contents($file, $content);
|
||||
|
||||
// test writing
|
||||
$this->assertNotFalse($bytesWritten);
|
||||
|
||||
Reference in New Issue
Block a user