diff --git a/.gitignore b/.gitignore index 6a3f5855..29f36a71 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/app/Events/UploadFile.php b/app/Events/UploadFile.php new file mode 100644 index 00000000..a414602b --- /dev/null +++ b/app/Events/UploadFile.php @@ -0,0 +1,42 @@ +uploadedFile = $uploadedFile; + $this->id = $id; + } + + /** + * Get the channels the event should be broadcast on. + * + * @return array + */ + public function broadcastOn() + { + return []; + } +} diff --git a/app/Http/Controllers/Admin/UploadFile.php b/app/Http/Controllers/Admin/UploadFile.php new file mode 100644 index 00000000..6dfc54f7 --- /dev/null +++ b/app/Http/Controllers/Admin/UploadFile.php @@ -0,0 +1,97 @@ +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); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Admin/Uploader.php b/app/Http/Controllers/Admin/Uploader.php deleted file mode 100644 index 8835782d..00000000 --- a/app/Http/Controllers/Admin/Uploader.php +++ /dev/null @@ -1,26 +0,0 @@ - 'required' - ]; + $this->rules = ['file' => 'required|image']; } } diff --git a/app/Http/Requests/UploadRequest.php b/app/Http/Requests/UploadRequest.php new file mode 100644 index 00000000..7d05410e --- /dev/null +++ b/app/Http/Requests/UploadRequest.php @@ -0,0 +1,32 @@ + 'max:255' + ]; + + // setup additional rules + $this->setRules(); + + foreach ($this->rules as $field => $rule) { + $rules[$field] = $rule; + } + + return $rules; + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index 141800a3..452b64b0 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -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 // ------------------------------ diff --git a/app/Listeners/LogUploadFile.php b/app/Listeners/LogUploadFile.php new file mode 100644 index 00000000..0ab6f838 --- /dev/null +++ b/app/Listeners/LogUploadFile.php @@ -0,0 +1,31 @@ +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; + } } \ No newline at end of file diff --git a/app/Providers/AdminMenusProvider.php b/app/Providers/AdminMenusProvider.php index 6328db93..bf4431c9 100644 --- a/app/Providers/AdminMenusProvider.php +++ b/app/Providers/AdminMenusProvider.php @@ -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']); diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 24f304ae..8bc512d8 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -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); - // } } diff --git a/config/app.php b/config/app.php index 0fd1d264..7bd0922e 100644 --- a/config/app.php +++ b/config/app.php @@ -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 diff --git a/database/migrations/2016_02_08_105722_AddExtendedFileInfo.php b/database/migrations/2016_02_08_105722_AddExtendedFileInfo.php new file mode 100644 index 00000000..3dde48b4 --- /dev/null +++ b/database/migrations/2016_02_08_105722_AddExtendedFileInfo.php @@ -0,0 +1,60 @@ +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'); + }); + } +} diff --git a/public/uploads/.gitkeep b/public/uploads/.gitkeep new file mode 100755 index 00000000..e69de29b diff --git a/resources/lang/en/admin/fileManager.php b/resources/lang/en/admin/fileManager.php new file mode 100644 index 00000000..31d17081 --- /dev/null +++ b/resources/lang/en/admin/fileManager.php @@ -0,0 +1,11 @@ + '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' +]; \ No newline at end of file diff --git a/resources/lang/en/admin/mainMenu.php b/resources/lang/en/admin/mainMenu.php index 69a371bb..d5e253b6 100644 --- a/resources/lang/en/admin/mainMenu.php +++ b/resources/lang/en/admin/mainMenu.php @@ -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' ]; \ No newline at end of file diff --git a/resources/views/admin/fileManager/newFile.blade.php b/resources/views/admin/fileManager/newFile.blade.php new file mode 100644 index 00000000..576762b9 --- /dev/null +++ b/resources/views/admin/fileManager/newFile.blade.php @@ -0,0 +1,14 @@ +@extends('admin.layout') + +@section('pageName', @trans('admin/fileManager.newFile')) + +@section('content') + +
+

@lang('admin/fileManager.newFile')

+
+ +
+ @include('admin.fileManager.newFileForm') +
+@stop \ No newline at end of file diff --git a/resources/views/admin/fileManager/newFileForm.blade.php b/resources/views/admin/fileManager/newFileForm.blade.php new file mode 100644 index 00000000..237fcf0e --- /dev/null +++ b/resources/views/admin/fileManager/newFileForm.blade.php @@ -0,0 +1,56 @@ +@if (session('success')) +
+ {{ session('success') }} +
+@endif + +@if (session('error')) +
+ {{ session('error') }} +
+@endif + +
+ {!! csrf_field() !!} + +
+
+ +
+
+ + + +
+
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/tests/WritePermissionsTest.php b/tests/WritePermissionsTest.php index 107304a4..7ff13f02 100644 --- a/tests/WritePermissionsTest.php +++ b/tests/WritePermissionsTest.php @@ -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);