From d40584de14adcffe150372622a314563ef836f6a Mon Sep 17 00:00:00 2001 From: ak Date: Tue, 17 Oct 2023 22:45:04 -0700 Subject: [PATCH] functional version --- app/Http/Controllers/FileController.php | 895 +++++++++++++++++- .../{StoreFolderRequest.php => NewFolder.php} | 6 +- .../{ParentIDBaseRequest.php => ParentId.php} | 2 +- app/Http/Requests/RecycledFiles.php | 33 + app/Http/Requests/SelectedFiles.php | 34 + app/Http/Requests/ShareFiles.php | 16 + app/Http/Requests/SharedFiles.php | 59 ++ app/Http/Requests/StoreFileRequest.php | 40 - app/Http/Requests/UploadFiles.php | 126 +++ app/Http/Resources/FileResource.php | 22 +- app/Mail/ShareFilesNotification.php | 55 ++ app/Models/File.php | 79 ++ app/Models/FileShare.php | 7 + app/Providers/RouteServiceProvider.php | 2 +- ...9_25_235150_create_starred_files_table.php | 4 +- ...9_192705_add_stored_at_column_to_files.php | 28 + docker-compose.yml | 4 +- docker/8.2/Dockerfile | 63 ++ docker/8.2/php.ini | 8 + docker/8.2/start-container | 17 + docker/8.2/supervisord.conf | 14 + docker/mysql/create-testing-database.sh | 6 + docker/pgsql/create-testing-database.sql | 2 + package-lock.json | 64 ++ package.json | 5 + public/images/application-octet-stream.svg | 13 + public/images/application-pdf.svg | 9 + public/images/ark.svg | 18 + public/images/audio-x-generic.svg | 9 + public/images/folder-blue.svg | 8 + public/images/image-x-generic.svg | 7 + public/images/text-x-generic.svg | 8 + public/images/video-x-generic.svg | 13 + public/images/x-office-document.svg | 11 + public/images/x-office-spreadsheet.svg | 9 + resources/css/app.css | 4 + resources/js/Components/NavLink.vue | 4 +- .../js/Components/custom/DeleteButton.vue | 41 + .../js/Components/custom/DownloadButton.vue | 128 +++ resources/js/Components/custom/FileIcon.vue | 73 ++ .../custom/FileUploadErrorModal.vue | 40 + .../custom/MainBreadcrumbButton.vue | 26 + resources/js/Components/custom/Navigation.vue | 22 +- .../js/Components/custom/NewDropdown.vue | 22 +- .../js/Components/custom/RecycleButton.vue | 49 + .../js/Components/custom/RestoreButton.vue | 40 + resources/js/Components/custom/SearchForm.vue | 25 +- .../js/Components/custom/ShareButton.vue | 39 + .../js/Components/custom/ShareFilesModal.vue | 108 +++ .../js/Components/custom/UnshareButton.vue | 51 + .../js/Components/custom/UploadFiles.vue | 2 +- .../js/Components/custom/UploadFolder.vue | 14 +- .../js/Components/custom/UserDropdown.vue | 6 +- resources/js/Layouts/AuthenticatedLayout.vue | 46 +- resources/js/Pages/Auth/Login.vue | 9 +- resources/js/Pages/Auth/Register.vue | 8 +- resources/js/Pages/Dashboard.vue | 28 - resources/js/Pages/RecycleBin.vue | 221 +++++ resources/js/Pages/SharedBy.vue | 258 +++++ resources/js/Pages/SharedWith.vue | 256 +++++ resources/js/Pages/UserFiles.vue | 273 ++++-- resources/js/Pages/Welcome.vue | 55 -- resources/js/app.js | 24 +- resources/js/getMimeType.js | 85 ++ resources/views/app.blade.php | 2 +- resources/views/mail/share.blade.php | 7 + routes/web.php | 25 +- 67 files changed, 3414 insertions(+), 273 deletions(-) rename app/Http/Requests/{StoreFolderRequest.php => NewFolder.php} (89%) rename app/Http/Requests/{ParentIDBaseRequest.php => ParentId.php} (96%) create mode 100644 app/Http/Requests/RecycledFiles.php create mode 100644 app/Http/Requests/SelectedFiles.php create mode 100644 app/Http/Requests/ShareFiles.php create mode 100644 app/Http/Requests/SharedFiles.php delete mode 100644 app/Http/Requests/StoreFileRequest.php create mode 100644 app/Http/Requests/UploadFiles.php create mode 100644 app/Mail/ShareFilesNotification.php create mode 100644 database/migrations/2023_10_09_192705_add_stored_at_column_to_files.php create mode 100644 docker/8.2/Dockerfile create mode 100644 docker/8.2/php.ini create mode 100644 docker/8.2/start-container create mode 100644 docker/8.2/supervisord.conf create mode 100644 docker/mysql/create-testing-database.sh create mode 100644 docker/pgsql/create-testing-database.sql create mode 100644 public/images/application-octet-stream.svg create mode 100644 public/images/application-pdf.svg create mode 100644 public/images/ark.svg create mode 100644 public/images/audio-x-generic.svg create mode 100644 public/images/folder-blue.svg create mode 100644 public/images/image-x-generic.svg create mode 100644 public/images/text-x-generic.svg create mode 100644 public/images/video-x-generic.svg create mode 100644 public/images/x-office-document.svg create mode 100644 public/images/x-office-spreadsheet.svg create mode 100644 resources/js/Components/custom/DeleteButton.vue create mode 100644 resources/js/Components/custom/DownloadButton.vue create mode 100644 resources/js/Components/custom/FileIcon.vue create mode 100644 resources/js/Components/custom/FileUploadErrorModal.vue create mode 100644 resources/js/Components/custom/MainBreadcrumbButton.vue create mode 100644 resources/js/Components/custom/RecycleButton.vue create mode 100644 resources/js/Components/custom/RestoreButton.vue create mode 100644 resources/js/Components/custom/ShareButton.vue create mode 100644 resources/js/Components/custom/ShareFilesModal.vue create mode 100644 resources/js/Components/custom/UnshareButton.vue delete mode 100644 resources/js/Pages/Dashboard.vue create mode 100644 resources/js/Pages/RecycleBin.vue create mode 100644 resources/js/Pages/SharedBy.vue create mode 100644 resources/js/Pages/SharedWith.vue delete mode 100644 resources/js/Pages/Welcome.vue create mode 100644 resources/js/getMimeType.js create mode 100644 resources/views/mail/share.blade.php diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php index 70fa70d..62da08e 100644 --- a/app/Http/Controllers/FileController.php +++ b/app/Http/Controllers/FileController.php @@ -2,17 +2,41 @@ namespace App\Http\Controllers; -use App\Http\Requests\StoreFolderRequest; -use App\Http\Requests\StoreFileRequest; + +use App\Http\Requests\NewFolder; +use App\Http\Requests\UploadFiles; +use App\Http\Requests\SelectedFiles; +use App\Http\Requests\RecycledFiles; +use App\Http\Requests\ShareFiles; +use App\Http\Requests\SharedFiles; + use App\Http\Resources\FileResource; -use App\Models\File; -use Inertia\Inertia; + +use Illuminate\Http\Request; + use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Str; + +use Inertia\Inertia; + +use App\Models\File; +use App\Models\User; +use App\Models\FileShare; + +use Carbon\Carbon; + +use Kalnoy\Nestedset\Collection; + +use ZipArchive; + +use App\Mail\ShareFilesNotification; class FileController extends Controller { // render user's files - public function userFiles(string $folder = null) { + public function userFiles(Request $request, string $folder = null) { // validate passed in $folder param // if passed in, if ($folder) { @@ -25,18 +49,25 @@ class FileController extends Controller // use root folder if $folder not passed in $folder = $this->getRoot(); } - // get all files and folders where parent_id = root - $files = File::query()->where('parent_id', $folder->id) - // that are created by the current user - ->where('created_by', Auth::id()) + + // get all files and folders created by current user + $files = File::query()->where('created_by', Auth::id()) // that have not been deleted ->whereNull('deleted_at') // order by folders first ->orderByDesc('is_folder') // then order by creation date - ->orderByDesc('created_at') - // return x amount (default 8) - ->paginate(8); + ->orderByDesc('created_at'); + + $search = $request->get('search'); + + if ($search) { + $files->where('name', 'like', $search); + } else { // return all files filtered by parent_id + $files->where('parent_id', $folder->id); + } + + $files = $files->get(); $files = FileResource::collection($files); @@ -47,30 +78,838 @@ class FileController extends Controller return Inertia::render('UserFiles', compact('files', 'folder', 'ancestors')); } - // create new folder based on passed in StoreFolderRequest - public function newFolder(StoreFolderRequest $request) { - // get validated data - $data = $request->validated(); - // get parent folder from StoreFolderRequest - $parent = $request->parent; - // check for parent - if (!$parent) { - $parent = $this->getRoot(); + // render files shared with user + public function sharedWithMe(Request $request, $folder = null) { + // validate passed in $folder param + if ($folder) { + // get ancestors + $firstQuery = File::query()->join('file_shares', 'file_shares.file_id', 'files.id') + ->where('file_shares.user_id', Auth::id()) + ->whereNull('files.deleted_at') + ->find(intval($folder)); // ensure that the specified id exists in the database + if ($firstQuery) { + $ancestors = $firstQuery->ancestors; + } + + // folder found in the shares db + $folder = File::query()->join('file_shares', 'file_shares.file_id', 'files.id') + ->where('file_shares.user_id', Auth::id()) + ->whereNull('files.deleted_at') + ->select('files.id as id', 'name', 'size', 'created_by', 'file_shares.created_at as created_at', 'file_shares.updated_at as updated_at', 'parent_id') + ->find(intval($folder)); // ensure that the specified id exists in the database + } + // if still exists, + if ($folder) { + // files are those found in the shares db + $files = File::query()->join('file_shares', 'file_shares.file_id', 'files.id') + // that are shared with the current user + ->where('file_shares.user_id', Auth::id()) + // that have not been deleted + ->whereNull('files.deleted_at') + // order by folders first + ->orderByDesc('files.is_folder') + // then order by creation date + ->orderByDesc('files.id') + // then select to reference file_id + ->select('files.id as id', 'name', 'size', 'created_by', 'file_shares.created_at as created_at', 'file_shares.updated_at as updated_at', 'mimetype', 'is_folder', 'parent_id'); + + $search = $request->get('search'); + + if ($search) { + $files->where('name', 'like', $search); + } else { // return all files filtered by $folder + $files->where('parent_id', $folder->id); + } + + $files = $files->get(); + + $files = FileResource::collection($files); + + // ancestors + $ancestors = FileResource::collection($ancestors); + + $folder = new FileResource($folder); + + // pass everything into SharedWith + return Inertia::render('SharedWith', compact('files', 'folder', 'ancestors')); + } + else { // no folder + // files are those found in the shares db + $files = File::query()->join('file_shares', 'file_shares.file_id', 'files.id') + // that are shared with the current user + ->where('file_shares.user_id', Auth::id()) + // that have not been deleted + ->whereNull('files.deleted_at') + // order by folders first + ->orderByDesc('files.is_folder') + // then order by creation date + ->orderByDesc('files.id') + ->select('files.id as id', 'name', 'size', 'created_by', 'file_shares.created_at as created_at', 'file_shares.updated_at as updated_at', 'mimetype', 'is_folder', 'parent_id'); + + $search = $request->get('search'); + + if ($search) { + $files->where('name', 'like', $search); + } + + $files = $files->get(); + + // make into collection + $files = FileResource::collection($files); + + $allIds = array(); + + // get $parentIds of each file + foreach($files as $file) { + array_push($allIds, intval($file->id)); + } + + // run through and filter the $files array to not have any files which are children of the displayed files + foreach($files as $key => $value) { + foreach($allIds as $allId) { + if ($value->parent_id == $allId) { + unset($files[$key]); + } + } + } + + // pass only files into SharedWith + return Inertia::render('SharedWith', compact('files')); } - $file = new File(); - $file->is_folder = 1; - $file->name = $data['name']; - $parent->appendNode($file); } - public function upload(StoreFileRequest $request) { + // render files shared by user + public function sharedByMe(Request $request, $folder = null) { + // validate passed in $folder param + if ($folder) { + // run a query just for ancestors + $firstQuery = File::query()->join('file_shares', 'file_shares.file_id', 'files.id') + ->where('files.created_by', Auth::id()) + ->whereNull('files.deleted_at') + ->find(intval($folder)); // ensure that the specified id exists in the database + if ($firstQuery) { + $ancestors = $firstQuery->ancestors; + } + + // folder found in the shares db + $folder = File::query()->join('file_shares', 'file_shares.file_id', 'files.id') + ->where('files.created_by', Auth::id()) + ->whereNull('files.deleted_at') + ->select('files.id as id', 'name', 'size', 'created_by', 'file_shares.created_at as created_at', 'file_shares.updated_at as updated_at', 'parent_id') + ->find(intval($folder)); // ensure that the specified id exists in the database + } + // if still exists, + if ($folder) { + // files are those found in the shares db + $files = File::query()->join('file_shares', 'file_shares.file_id', 'files.id') + // that are made by the current user + ->where('files.created_by', Auth::id()) + // that have not been deleted + ->whereNull('files.deleted_at') + // order by folders first + ->orderByDesc('files.is_folder') + // then order by creation date + ->orderByDesc('files.id') + // then select to reference file_id + ->select('files.id as id', 'name', 'size', 'created_by', 'file_shares.created_at as created_at', 'file_shares.updated_at as updated_at', 'mimetype', 'is_folder', 'parent_id', 'file_shares.user_id as user_id'); + + $search = $request->get('search'); + + if ($search) { + $files->where('name', 'like', $search); + } else { // return all files filtered by $folder + $files->where('parent_id', $folder->id); + } + + $files = $files->get(); + + $files = FileResource::collection($files); + + // get shared_with name for each file + foreach($files as $file) { + $user = User::query()->where('id', $file->user_id)->first(); + $file->shared_with = $user->name; + } + + // ancestors + $ancestors = FileResource::collection($ancestors); + + $folder = new FileResource($folder); + + // pass everything into SharedBy + return Inertia::render('SharedBy', compact('files', 'folder', 'ancestors')); + } + else { // no folder + // files are those found in the shares db + $files = File::query()->join('file_shares', 'file_shares.file_id', 'files.id') + // that are made by the current user + ->where('files.created_by', Auth::id()) + // that have not been deleted + ->whereNull('files.deleted_at') + // order by folders first + ->orderByDesc('files.is_folder') + // then order by creation date + ->orderByDesc('files.id') + ->select('files.id as id', 'name', 'size', 'created_by', 'file_shares.created_at as created_at', 'file_shares.updated_at as updated_at', 'mimetype', 'is_folder', 'parent_id', 'file_shares.user_id as user_id'); + + $search = $request->get('search'); + + if ($search) { + $files->where('name', 'like', $search); + } + + $files = $files->get(); + + // get shared_with name for each file + foreach($files as $file) { + $user = User::query()->where('id', $file->user_id)->first(); + $file->shared_with = $user->name; + } + + // make into collection + $files = FileResource::collection($files); + + $allIds = array(); + + // get $parentIds of each file + foreach($files as $file) { + array_push($allIds, intval($file->id)); + } + + // run through and filter the $files array to not have any files which are children of the displayed files + foreach($files as $key => $value) { + foreach($allIds as $allId) { + if ($value->parent_id == $allId) { + unset($files[$key]); + } + } + } + + // pass only files into SharedBy + return Inertia::render('SharedBy', compact('files')); + } + } + + // render recycle bin + public function recycleBin(Request $request, $folder = null) { + // validate passed in $folder param + if ($folder) { + // find folder + $folder = File::onlyTrashed() + // made by current user + ->where('created_by', Auth::id()) + ->find(intval($folder)); + } + // if still exists, + if ($folder) { + // files are those that are trashed + $files = File::onlyTrashed() + // made by current user + ->where('created_by', Auth::id()) + ->orderBy('is_folder', 'desc') + ->orderBy('deleted_at', 'desc') + ->orderBy('files.id', 'desc'); + + $search = $request->get('search'); + + if ($search) { + $files->where('name', 'like', $search); + } else { // return all files filtered by $folder + $files->where('parent_id', $folder->id); + } + + $files = $files->get(); + + $files = FileResource::collection($files); + + function recursiveAncestors($file, &$ancestors) { + $parentFile = File::onlyTrashed() + ->where('created_by', Auth::id()) + ->find($file->parent_id); + + if ($parentFile) { + array_unshift($ancestors, $parentFile); + if ($parentFile->parent_id) { + recursiveAncestors($parentFile, $ancestors); + } + } + } + $ancestors = []; + recursiveAncestors($folder, $ancestors); + // ancestors + $ancestors = FileResource::collection([...$ancestors, $folder]); + + $folder = new FileResource($folder); + + // pass everything into RecycleBin + return Inertia::render('RecycleBin', compact('files', 'folder', 'ancestors')); + } + else { // no folder + // files are those that are trashed + $files = File::onlyTrashed() + // made by current user + ->where('created_by', Auth::id()) + ->orderBy('is_folder', 'desc') + ->orderBy('deleted_at', 'desc') + ->orderBy('files.id', 'desc'); + + $search = $request->get('search'); + + if ($search) { + $files->where('name', 'like', $search); + } + + $files = $files->get(); + + // make into collection + $files = FileResource::collection($files); + + $allIds = array(); + + // get $parentIds of each file + foreach($files as $file) { + array_push($allIds, intval($file->id)); + } + + // run through and filter the $files array to not have any files which are children of the displayed files + foreach($files as $key => $value) { + foreach($allIds as $allId) { + if ($value->parent_id == $allId) { + unset($files[$key]); + } + } + } + + // pass only files into SharedBy + return Inertia::render('RecycleBin', compact('files')); + } + } + + // create new folder based on passed in NewFolder request + public function newFolder(NewFolder $request) { // get validated data $data = $request->validated(); - // get parent folder from StoreFolderRequest - dd ($data); + // get parent folder from NewFolder request + $parent = $request->parent; + // make new folder object based on File() object + $folder = new File(); + $folder->is_folder = 1; + $folder->name = $data['name']; + + $parent->appendNode($folder); } + // upload files + public function upload(UploadFiles $request) { + // get validated data + $data = $request->validated(); + // get parent folder from UploadFiles request + $parent = $request->parent; + // get file tree structure from UploadFiles request + $tree = $request->file_tree; + // if the tree is empty, then files were uploaded without any folder structure - each file needs to be uploaded + // otherwise, the entire folder structure needs to be uploaded + empty($tree) ? $this->uploadFiles($data['files'], $parent) : $this->uploadTree($tree, $parent); + } + + // get download link for selected files + public function download(SelectedFiles $request) { + $data = $request->validated(); + // get parent folder from DeleteFiles request + $parent = $request->parent; + // get selected Id's otherwise empty array + $Ids = $data['Ids'] ?? []; + + // if no files selected + if (!$data['all'] == false && empty($Ids)) { + return [ + 'message' => 'No files selected!' + ]; + } + + // if all selected, download all + if ($data['all']) { + $downloadURL = $this->makeArchive($parent->children); + // make filename for file + $filename = $parent->name . ".zip"; + } + // else download the selected IDs + else { + // if there is only one ID selected + if (count($Ids) === 1) { + // get file + $file = File::find($Ids[0]); + // if file is a folder + if ($file->is_folder) { + // and folder is empty + if($file->children->isEmpty()) { + // return message + return [ + "message" => "This folder is empty" + ]; + } + // else folder is populated + $files = $file->children; + $downloadURL = $this->makeArchive($files); + $filename = $file->name . ".zip"; + } + // otherwise + else { + // create a new path off "public/" + $path = "public/" . pathinfo($file->stored_at, PATHINFO_BASENAME); + // copy file to path + Storage::copy($file->stored_at, $path); + // define download path + $downloadURL = asset(Storage::url($path)); + // define filename + $filename = $file->name; + } + } + // otherwise there are many IDs selected + else { + $files = File::query()->whereIn('id', $Ids)->get(); + $downloadURL = $this->makeArchive($files); + $filename = $parent->name . ".zip"; + } + } + + // return URL and filename + return [ + 'url' => $downloadURL, + 'filename' => $filename, + ]; + } + + // get download link for files that are shared + public function downloadShared(SharedFiles $request) { + $data = $request->validated(); + // get parent folder from DeleteFiles request + $parent = $request->parent; + // get selected Id's otherwise empty array + $Ids = $data['Ids'] ?? []; + // if no files selected + if (!$data['all'] == false && empty($Ids)) { + return [ + 'message' => 'No files selected!' + ]; + } + + if ($parent) { + // if all selected, download all + if ($data['all']) { + $downloadURL = $this->makeArchive($parent->children); + // make filename for file + $filename = $parent->name . ".zip"; + } + + // else download the selected IDs + else { + // if there is only one ID selected + if (count($Ids) === 1) { + // get file + $file = File::find($Ids[0]); + // if file is a folder + if ($file->is_folder) { + // and folder is empty + if ($file->children->isEmpty()) { + // return message + return [ + "message" => "This folder is empty" + ]; + } + // else folder is populated + $files = $file->children; + $downloadURL = $this->makeArchive($files); + $filename = $file->name . ".zip"; + } + // otherwise + else { + // create a new path off "public/" + $path = "public/" . pathinfo($file->stored_at, PATHINFO_BASENAME); + // copy file to path + Storage::copy($file->stored_at, $path); + // define download path + $downloadURL = asset(Storage::url($path)); + // define filename + $filename = $file->name; + } + } + // otherwise there are many IDs selected + else { + $files = File::query()->whereIn('id', $Ids)->get(); + $downloadURL = $this->makeArchive($files); + $filename = $parent->name . ".zip"; + } + } + } + // else there is no $parent + else { + // if all selected, download all + if ($data['all']) { + // files are those found in the shares db + $files = File::query()->join('file_shares', 'file_shares.file_id', 'files.id') + // that are shared with the current user + ->where('file_shares.user_id', Auth::id()) + // that have not been deleted + ->whereNull('files.deleted_at') + // order by folders first + ->orderByDesc('files.is_folder') + // then order by creation date + ->orderByDesc('files.id') + ->get(); + $downloadURL = $this->makeArchive($files); + // make filename for file + $filename = "shared-" . Str::random(10) . ".zip"; + } + + // else download the selected IDs + else { + // if there is only one ID selected + if (count($Ids) === 1) { + // get file + $file = File::find($Ids[0]); + // if file is a folder + if ($file->is_folder) { + // and folder is empty + if ($file->children->isEmpty()) { + // return message + return [ + "message" => "This folder is empty" + ]; + } + // else folder is populated + $files = $file->children; + $downloadURL = $this->makeArchive($files); + $filename = $file->name . ".zip"; + } + // otherwise + else { + // create a new path off "public/" + $path = "public/" . pathinfo($file->stored_at, PATHINFO_BASENAME); + // copy file to path + Storage::copy($file->stored_at, $path); + // define download path + $downloadURL = asset(Storage::url($path)); + // define filename + $filename = $file->name; + } + } + // otherwise there are many IDs selected + else { + $files = File::query()->whereIn('id', $Ids)->get(); + $downloadURL = $this->makeArchive($files); + $filename = "shared-" . Str::random(10) . ".zip"; + } + } + } + + // return URL and filename + return [ + 'url' => $downloadURL, + 'filename' => $filename, + ]; + } + + // recycle selected files + public function recycle(SelectedFiles $request) { + // get validated data + $data = $request->validated(); + // get parent folder from DeleteFiles request + $parent = $request->parent; + + // if all files were selected + if ($data['all']) { + foreach ($parent->children as $file) { + $file->recycle(); + } + } + // otherwise specific IDs were selected + else { + foreach($data['Ids'] ?? [] as $fileId) { + $targetFile = File::find($fileId); + if ($targetFile) $targetFile->delete(); + } + } + + return to_route('userFiles', [ + 'folder' => $parent->path + ]); + } + + // restores selected files + public function restore(RecycledFiles $request) { + // get validated data + $data = $request->validated(); + + // if all files were selected + if ($data['all']) { + $files = File::onlyTrashed()->get(); + foreach ($files as $file) { + $file->restore(); + } + } + // otherwise specific IDs were selected + else { + // get Id's unless empty + $Ids = $data['Ids'] ?? []; + + // get files + $files = File::onlyTrashed() + ->whereIn('id', $Ids) + ->get(); + + foreach ($files as $file) { + $file->restore(); + } + } + + return to_route('recycleBin'); + } + + // deletes selected files (permanent delete) + public function delete(RecycledFiles $request) { + // get validated data + $data = $request->validated(); + + // if all files were selected + if ($data['all']) { + $files = File::onlyTrashed()->get(); + foreach ($files as $file) { + $file->shred(); + } + } + // otherwise specific IDs were selected + else { + // get Id's unless empty + $Ids = $data['Ids'] ?? []; + + // get files + $files = File::onlyTrashed() + ->whereIn('id', $Ids) + ->get(); + + foreach ($files as $file) { + $file->shred(); + } + } + + return to_route('recycleBin'); + } + + // shares selected files + public function share(ShareFiles $request) { + // get data + $data = $request->validated(); + // get parent folder + $parent = $request->parent; + // get selected Id's otherwise empty array + $Ids = $data['Ids'] ?? []; + // get user by email + $user = User::query()->where('email', $data['email'])->first(); + + // if no files selected + if (!$data['all'] == false && empty($Ids)) { + return [ + 'message' => 'No files selected!' + ]; + } + + // if user doesn't exist + if (!$user->id) { + return [ + 'message' => 'Email does not belong to user!' + ]; + } + + // otherwise, if all is passed, get current children. else find files by passed Id's + $files = $data['all'] ? $parent->children : File::find($Ids); + + // define recursive function to add files to share + function recursiveShare($queued, $recipient) { + // get children of parent and determine whether recursive function needs to be called + foreach ($queued as $file) { + // check if file already exists in shared DB + $exists = FileShare::query()->where('file_id', $file->id)->where('user_id', $recipient->id)->first(); + if ($exists) { + continue; + } + // else + // create new entry + $entry = [ + 'file_id' => $file->id, + 'user_id' => $recipient->id, + 'created_at' => $file->created_at, + 'updated_at' => Carbon::now(), + ]; + FileShare::insert($entry); + + // recurse on children, if any + if ($file->children) { + recursiveShare($file->children, $recipient); + } + } + } + + // begin recursion on files + recursiveShare($files, $user); + + // send email to recipient + Mail::to($user)->send(new ShareFilesNotification($user, Auth::user(), $files)); + + return redirect()->back(); + } + + + // unshares selected files + public function unshare(SharedFiles $request) { + // get data + $data = $request->validated(); + // get parent folder + $parent = $request->parent; + + // get selected Id's otherwise empty array + $Ids = $data['Ids'] ?? []; + + // if no files selected + if (!$data['all'] == false && empty($Ids)) { + return [ + 'message' => 'No files selected!' + ]; + } + + // if parent exists + if ($parent) { + // if all is passed, get current children. else find files by passed Id's + $files = $data['all'] ? $parent->children : File::find($Ids); + } + // else + else { + // find all files that are created by current user and shared with others + $all = File::query()->join('file_shares', 'file_shares.file_id', 'files.id') + ->where('files.created_by', Auth::id()) + ->whereNull('files.deleted_at') + ->get(); + + $files = $data['all'] ? $all : File::find($Ids); + } + + // define recursive function to remove files to share + function recursiveUnshare($queued) { + // get children of parent and determine whether recursive function needs to be called + foreach ($queued as $file) { + // check if file already exists in shared DB + $dbRecord = FileShare::query()->where('file_id', $file->id)->first(); + if ($dbRecord) { + // get regular db file + $file = File::query()->where('created_by', Auth::id())->find($file->id); + $dbRecord->forceDelete(); + + // recurse on children, if any + if ($file->children) { + recursiveUnshare($file->children); + } + } + } + } + // begin recursion on files + recursiveUnshare($files); + + return redirect()->back(); + } + + // gets user's root folder - called from userFiles private function getRoot() { return File::query()->whereIsRoot()->where('created_by', Auth::id())->firstOrFail(); } + + // upload files - called from upload + private function uploadFiles($files, $parent) { + // loop over files, make and upload each one + foreach ($files as $userFile) { + $file = new File(); + $file->stored_at = $userFile->store('/files/'.Auth::id()); + $file->is_folder = false; + $file->name = $userFile->getClientOriginalName(); + $file->mimetype = $userFile->getMimeType(); + $file->size = $userFile->getSize(); + $parent->appendNode($file); + } + } + + // upload tree - called from upload + private function uploadTree($tree, $parent) { + // loop over files in file tree + foreach($tree as $name => $file) { + // if current node in $tree is an array (that means, a folder) + if (is_array($file)) { + // make folder + $folder = new File(); + $folder->is_folder = 1; + $folder->name = $name; + // append folder to parent + $parent->appendNode($folder); + // call function recursively until if condition is false + // each call has the newly-created $folder as $parent + $this->uploadTree($file, $folder); + } + // otherwise the node is a file; use uploadFiles() with file + else { + $this->uploadFiles([$file], $parent); + } + } + } + + // makes archive - called from download + private function makeArchive(Collection $files): string + { + // make name for archive + $archiveName = Str::random() . ".zip"; + // create public path + $path = "public/$archiveName"; + + // if the current $path is NOT already being used + if (!is_dir(dirname($path))) { + Storage::makeDirectory(dirname($path)); + } + + // get the absolute path of $path + $path = Storage::path($path); + // create new ZipArchive + $archive = new ZipArchive(); + // see if a archive can be created or overwritten at $path + $res = $archive->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE); + // if so + if ($res === true) { + // define recursive function to add files to archive + function recursiveAdd($queued, $archive, $parent = "") { + // get children of parent and determine whether recursive function needs to be called + foreach ($queued as $file) { + // if there are no children, delete file + if ($file->children->isEmpty()) { + // set $internalPath to reflect file + $internalPath = $parent . $file->name; + // add file to archive + $archive->addFile(Storage::path($file->stored_at), $internalPath); + // continue loop + continue; + } + // else + // set $internalPath to reflect a directory + $internalPath = $parent . $file->name . "/"; + // recurse + recursiveAdd($file->children, $archive, $internalPath); + } + } + + // add all $files to $archive + recursiveAdd($files, $archive); + } + + // close archive + $archive->close(); + + // return archive + return asset(Storage::url($archiveName)); + } + } diff --git a/app/Http/Requests/StoreFolderRequest.php b/app/Http/Requests/NewFolder.php similarity index 89% rename from app/Http/Requests/StoreFolderRequest.php rename to app/Http/Requests/NewFolder.php index b86e664..89bf2ea 100644 --- a/app/Http/Requests/StoreFolderRequest.php +++ b/app/Http/Requests/NewFolder.php @@ -2,14 +2,14 @@ namespace App\Http\Requests; -use App\Http\Requests\ParentIDBaseRequest; +use App\Http\Requests\ParentId; use App\Models\File; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\Rule; -class StoreFolderRequest extends ParentIDBaseRequest +class NewFolder extends ParentId { - // authorization handled by ParentIDBaseRequest + // authorization handled by ParentId /** * Get the validation rules that apply to the request. diff --git a/app/Http/Requests/ParentIDBaseRequest.php b/app/Http/Requests/ParentId.php similarity index 96% rename from app/Http/Requests/ParentIDBaseRequest.php rename to app/Http/Requests/ParentId.php index c071296..e5fc028 100644 --- a/app/Http/Requests/ParentIDBaseRequest.php +++ b/app/Http/Requests/ParentId.php @@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Auth; use App\Models\File; -class ParentIDBaseRequest extends FormRequest +class ParentId extends FormRequest { public ?File $parent = null; diff --git a/app/Http/Requests/RecycledFiles.php b/app/Http/Requests/RecycledFiles.php new file mode 100644 index 0000000..5f0b9c2 --- /dev/null +++ b/app/Http/Requests/RecycledFiles.php @@ -0,0 +1,33 @@ +|string> + */ + public function rules(): array + { + // append rules to default validation rules + return [ + 'all' => 'bool', // all or not + 'Ids.*' => Rule::exists('files', 'id') // check that file exists in database + ->where(fn($query) => $query->where('created_by', Auth::id())) // check file was made by current user + ]; + } +} diff --git a/app/Http/Requests/SelectedFiles.php b/app/Http/Requests/SelectedFiles.php new file mode 100644 index 0000000..3a2469b --- /dev/null +++ b/app/Http/Requests/SelectedFiles.php @@ -0,0 +1,34 @@ +merge([ + 'all' => filter_var($this->all, FILTER_VALIDATE_BOOL), + ]); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + // append rules to default validation rules + return array_merge(parent::rules(), [ + 'all' => 'bool', // all or not + 'Ids.*' => Rule::exists('files', 'id') // check that file exists in database + ->where(fn($query) => $query->where('created_by', Auth::id())) // check file was made by current user + ]); + } +} diff --git a/app/Http/Requests/ShareFiles.php b/app/Http/Requests/ShareFiles.php new file mode 100644 index 0000000..f8ff404 --- /dev/null +++ b/app/Http/Requests/ShareFiles.php @@ -0,0 +1,16 @@ + 'required|email', // email or not + ]); + } +} diff --git a/app/Http/Requests/SharedFiles.php b/app/Http/Requests/SharedFiles.php new file mode 100644 index 0000000..c007edb --- /dev/null +++ b/app/Http/Requests/SharedFiles.php @@ -0,0 +1,59 @@ +parent = File::query()->where('id', $this->input('parent_id'))->first(); + // continue + return true; + } + + protected function prepareForValidation() { + // cast to boolean + $this->merge([ + 'all' => filter_var($this->all, FILTER_VALIDATE_BOOL), + ]); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + if ($this->parent) { + // validate + return [ + 'parent_id' => [ + Rule::exists(File::class, 'id') + ->where(function (Builder $query) { + return $query + ->where('is_folder', '1'); // must be a folder + } + ) + ], + 'all' => 'bool', // all or not + 'Ids.*' => Rule::exists('files', 'id'), // check that files exist in database + ]; + } + else { + return [ + 'all' => 'bool', // all or not + 'Ids.*' => Rule::exists('files', 'id'), // check that file exists in database + ]; + } + } +} diff --git a/app/Http/Requests/StoreFileRequest.php b/app/Http/Requests/StoreFileRequest.php deleted file mode 100644 index b17688d..0000000 --- a/app/Http/Requests/StoreFileRequest.php +++ /dev/null @@ -1,40 +0,0 @@ -|string> - */ - public function rules(): array - { - return array_merge(parent::rules(), - [ - 'files.*' => [ - 'required', - 'file', - function ($attribute, $value, $fail) { - $file = File::query() - ->where('name', $value->getClientOriginalName()) - ->where('created_by', Auth::id()) - ->where('parent_id', $this->parent_id) - ->whereNull('deleted_at'); - - if ($file->exists()) { - $fail('File already exists.'); - } - } - ], - ]); - } -} diff --git a/app/Http/Requests/UploadFiles.php b/app/Http/Requests/UploadFiles.php new file mode 100644 index 0000000..fa9bbd3 --- /dev/null +++ b/app/Http/Requests/UploadFiles.php @@ -0,0 +1,126 @@ +paths ?? [], + fn($f) => $f != null + ); + + $this->merge([ + 'file_paths' => $paths, + 'folder_name' => $this->getFolder($paths) + ]); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return array_merge(parent::rules(), + [ + 'files.*' => [ + 'required', + 'file', + function ($attribute, $value, $fail) { + // if this is not a folder, apply file validation to file + if (!$this->folder_name) { + // find if a file with the same parameters already exists + $file = File::query() + ->where('name', $value->getClientOriginalName()) + ->where('created_by', Auth::id()) + ->where('parent_id', $this->parent_id) + ->whereNull('deleted_at'); + + if ($file->exists()) { + $fail('File already exists.'); + } + } + + } + ], + 'folder_name' => [ + 'nullable', + 'string', + function ($attribute, $value, $fail) { + // if the folder name was passed in, that is if the path was not empty + if ($value) { + // find if a folder with the same parameters already exists + $folder = File::query() + ->where('name', $value) + ->where('created_by', Auth::id()) + ->where('parent_id', $this->parent_id) + ->whereNull('deleted_at'); + + if ($folder->exists()) { + $fail('Folder already exists.'); + } + } + } + ] + ]); + } + + protected function passedValidation() { + // get validated data + $data = $this->validated(); + + // replace object with file tree (function below) + $this->replace([ + 'file_tree' => $this->makeTree($this->file_paths, $data['files']) + ]); + } + + // returns folder path for uploaded file + public function getFolder($paths) { + if (!$paths) return null; + $url = explode("/", $paths[0]); + return $url[0]; + } + + // makes file tree from paths and files + private function makeTree($paths, $files) { + // match $paths array count to $files array count in case there is a difference between the two + $paths = array_slice($paths, 0, count($files)); + // filter away empty values + $paths = array_filter($paths, fn($path) => $path != null); + // build the tree + $fileTree = []; + + foreach ($paths as $index => $path) { + // get full path as array of url components separated by slashes + $url = explode("/", $path); + $node = &$fileTree; // get node on file tree - passed by reference for modification + + foreach ($url as $urlIndex => $urlComponent) { + // if there is no node value at the current $urlIndex, make empty array for value + // example: /test/file.png + // $node[$urlComponent] at test becomes empty array holding the files inside + if(!isset($node[$urlComponent])) { + $node[$urlComponent] = []; + } + + // if at the last index in $url, the current index is the uploaded file itself + // file can be found at the files array at the same index as $index since $paths has been filtered + // otherwise, set $node to current node and continue loop + $urlIndex == (count($url) - 1) ? $node[$urlComponent] = $files[$index] : $node = &$node[$urlComponent]; + } + } + + // return completed $fileTree after loop complete and nodes are made for every file + return $fileTree; + } +} diff --git a/app/Http/Resources/FileResource.php b/app/Http/Resources/FileResource.php index afede68..3829e7e 100644 --- a/app/Http/Resources/FileResource.php +++ b/app/Http/Resources/FileResource.php @@ -16,20 +16,36 @@ class FileResource extends JsonResource */ public function toArray(Request $request): array { + // get deleted at + $deletedAt = $this->deleted_at; + // make date readable if it is not null + if ($deletedAt) { + $deletedAt = $deletedAt->diffForHumans(); + } + + $size = $this->size; + // get if folder + $isFolder = $this->is_folder; + // if it is, set $size to getFolderSize() + if ($isFolder) { + $size = $this->getFolderSize(); + } + return [ - "mime" => $this->mime, + "mime" => $this->mimetype, "path" => $this->path, "id" => $this->id, "parent_id" => $this->parent_id, "name" => $this->name, - "size" => $this->size, + "size" => $this->formatSize($size), "owner" => $this->user->name, "is_folder" => $this->is_folder, "created_at" => $this->created_at->diffForHumans(), "updated_at" => $this->updated_at->diffForHumans(), "created_by" => $this->created_by, "updated_by" => $this->updated_by, - "deleted_at" => $this->deleted_at, + "deleted_at" => $deletedAt, + "shared_with" => $this->shared_with ? $this->shared_with : null, ]; } } diff --git a/app/Mail/ShareFilesNotification.php b/app/Mail/ShareFilesNotification.php new file mode 100644 index 0000000..31d778e --- /dev/null +++ b/app/Mail/ShareFilesNotification.php @@ -0,0 +1,55 @@ + + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/File.php b/app/Models/File.php index 6a89245..9137673 100644 --- a/app/Models/File.php +++ b/app/Models/File.php @@ -7,8 +7,11 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Carbon; use Illuminate\Support\Str; use Kalnoy\Nestedset\NodeTrait; +use Illuminate\Support\Facades\Storage; +use Kalnoy\NestedSet\DescendantsRelation; class File extends Model { @@ -36,6 +39,35 @@ class File extends Model return $this->created_by = $userID; } + // returns formatted size + public function formatSize($size) { + // maximum file size is in the GB range + $sizeUnits = ['B', 'KB', 'MB', 'GB']; + // floor() the size and get the amount of times 1024 fits into it + $power = $size > 0 ? floor(log($size, 1024)) : 0; + // calculate output by dividing total size by $power * 1024 then adding relevant $sizeUnit based on $power ($power can be no greater than 3 - no bigger than GB's) + $output = number_format(($size / pow(1024, $power)), 2, ".", ",") . " " . $sizeUnits[$power]; + return $output; + } + + // outputs correct sizes for folders + public function getFolderSize() { + $sum = []; + // call recursive function, then sum + $this->recurseChildren($this->children, $sum); + $sum = array_sum($sum); + // return $sum + return $sum; + } + + // shred $this file + public function shred() { + // recursively shred all of $this + $this->recursiveShred([$this]); + // also call force delete + $this->forceDelete(); + } + // additional bootstrapping on top of default Model model protected static function boot() { @@ -58,5 +90,52 @@ class File extends Model // append current file or folder name to path name $model->path = $model->path . Str::slug($model->name); }); + + // define delete function + static::deleted(function (File $file) { + // if file is a regular file (not a folder) + if (!$file->is_folder) { + // delete file from file system + Storage::delete($file->stored_at); + } + }); + } + + // moves $this file to recycle bin + private function recycle() { + // set this file's deleted_at to now + $this->deleted_at = Carbon::now(); + // save record + return $this->save(); + } + + // recursively shred $queued files + private function recursiveShred($queued) { + // get children of parent and determine whether recursive function needs to be called + foreach ($queued as $file) { + // if there are no children, delete file + if ($file->children->isEmpty()) { + Storage::delete($file->stored_at); + // continue loop + continue; + } + // else recurse + $this->recursiveShred($file->children); + } + } + + // get all children and subchildren + private function recurseChildren ($files, &$total) { + // go through each file in queue + foreach($files as $file) { + // push to array since it is a child + array_push($total, $file->size); + // iterate to next file in $queue if no more children + if (!$file->children) { + continue; + } + // else recurse on $current file children + $this->recurseChildren($file->children, $total); + } } } diff --git a/app/Models/FileShare.php b/app/Models/FileShare.php index 3255dec..13a4ad3 100644 --- a/app/Models/FileShare.php +++ b/app/Models/FileShare.php @@ -8,4 +8,11 @@ use Illuminate\Database\Eloquent\Model; class FileShare extends Model { use HasFactory; + + protected $fillable = [ + 'file_id', + 'user_id', + 'created_at', + 'updated_at' + ]; } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 4238c6e..7324654 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider * * @var string */ - public const HOME = '/my-files'; + public const HOME = '/files'; /** * Define your route model bindings, pattern filters, and other route configuration. diff --git a/database/migrations/2023_09_25_235150_create_starred_files_table.php b/database/migrations/2023_09_25_235150_create_starred_files_table.php index dd59b00..bb3b93d 100644 --- a/database/migrations/2023_09_25_235150_create_starred_files_table.php +++ b/database/migrations/2023_09_25_235150_create_starred_files_table.php @@ -13,8 +13,8 @@ return new class extends Migration { Schema::create('starred_files', function (Blueprint $table) { $table->id(); - $table->foreignId('fileID')->constrained('files'); - $table->foreignId('userID')->constrained('users'); + $table->foreignId('file_id')->constrained('files'); + $table->foreignId('user_id')->constrained('users'); $table->timestamps(); }); } diff --git a/database/migrations/2023_10_09_192705_add_stored_at_column_to_files.php b/database/migrations/2023_10_09_192705_add_stored_at_column_to_files.php new file mode 100644 index 0000000..9f87106 --- /dev/null +++ b/database/migrations/2023_10_09_192705_add_stored_at_column_to_files.php @@ -0,0 +1,28 @@ +string('stored_at', 2000)->nullable()->after('path'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('files', function (Blueprint $table) { + $table->dropColumn(('stored_at')); + }); + } +}; diff --git a/docker-compose.yml b/docker-compose.yml index fea7dc9..974fe05 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: laravel.test: build: - context: ./vendor/laravel/sail/runtimes/8.2 + context: ./docker/8.2 dockerfile: Dockerfile args: WWWGROUP: '${WWWGROUP}' @@ -40,7 +40,7 @@ services: MYSQL_ALLOW_EMPTY_PASSWORD: 1 volumes: - 'sail-mysql:/var/lib/mysql' - - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh' + - './docker/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh' networks: - sail healthcheck: diff --git a/docker/8.2/Dockerfile b/docker/8.2/Dockerfile new file mode 100644 index 0000000..239f0f0 --- /dev/null +++ b/docker/8.2/Dockerfile @@ -0,0 +1,63 @@ +FROM ubuntu:22.04 + +LABEL maintainer="Taylor Otwell" + +ARG WWWGROUP +ARG NODE_VERSION=18 +ARG POSTGRES_VERSION=15 + +WORKDIR /var/www/html + +ENV DEBIAN_FRONTEND noninteractive +ENV TZ=UTC + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update \ + && mkdir -p /etc/apt/keyrings \ + && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 dnsutils librsvg2-bin fswatch \ + && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \ + && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ + && apt-get update \ + && apt-get install -y php8.2-cli php8.2-dev \ + php8.2-pgsql php8.2-sqlite3 php8.2-gd php8.2-imagick \ + php8.2-curl \ + php8.2-imap php8.2-mysql php8.2-mbstring \ + php8.2-xml php8.2-zip php8.2-bcmath php8.2-soap \ + php8.2-intl php8.2-readline \ + php8.2-ldap \ + php8.2-msgpack php8.2-igbinary php8.2-redis php8.2-swoole \ + php8.2-memcached php8.2-pcov php8.2-xdebug \ + && curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \ + && apt-get update \ + && apt-get install -y nodejs \ + && npm install -g npm \ + && npm install -g pnpm \ + && npm install -g bun \ + && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \ + && echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \ + && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ + && apt-get update \ + && apt-get install -y yarn \ + && apt-get install -y mysql-client \ + && apt-get install -y postgresql-client-$POSTGRES_VERSION \ + && apt-get -y autoremove \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.2 + +RUN groupadd --force -g $WWWGROUP sail +RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail + +COPY start-container /usr/local/bin/start-container +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY php.ini /etc/php/8.2/cli/conf.d/99-sail.ini +RUN chmod +x /usr/local/bin/start-container + +EXPOSE 8000 + +ENTRYPOINT ["start-container"] diff --git a/docker/8.2/php.ini b/docker/8.2/php.ini new file mode 100644 index 0000000..cdd94e6 --- /dev/null +++ b/docker/8.2/php.ini @@ -0,0 +1,8 @@ +[PHP] +post_max_size = 2G +upload_max_filesize = 2G +max_file_uploads = 500 +variables_order = EGPCS + +[opcache] +opcache.enable_cli=1 diff --git a/docker/8.2/start-container b/docker/8.2/start-container new file mode 100644 index 0000000..b864399 --- /dev/null +++ b/docker/8.2/start-container @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +if [ ! -z "$WWWUSER" ]; then + usermod -u $WWWUSER sail +fi + +if [ ! -d /.composer ]; then + mkdir /.composer +fi + +chmod -R ugo+rw /.composer + +if [ $# -gt 0 ]; then + exec gosu $WWWUSER "$@" +else + exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf +fi diff --git a/docker/8.2/supervisord.conf b/docker/8.2/supervisord.conf new file mode 100644 index 0000000..9d28479 --- /dev/null +++ b/docker/8.2/supervisord.conf @@ -0,0 +1,14 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:php] +command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 +user=sail +environment=LARAVEL_SAIL="1" +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/docker/mysql/create-testing-database.sh b/docker/mysql/create-testing-database.sh new file mode 100644 index 0000000..aeb1826 --- /dev/null +++ b/docker/mysql/create-testing-database.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL + CREATE DATABASE IF NOT EXISTS testing; + GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%'; +EOSQL diff --git a/docker/pgsql/create-testing-database.sql b/docker/pgsql/create-testing-database.sql new file mode 100644 index 0000000..d84dc07 --- /dev/null +++ b/docker/pgsql/create-testing-database.sql @@ -0,0 +1,2 @@ +SELECT 'CREATE DATABASE testing' +WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'testing')\gexec diff --git a/package-lock.json b/package-lock.json index 380e27b..4070128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,8 +5,13 @@ "packages": { "": { "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.4.2", + "@fortawesome/free-regular-svg-icons": "^6.4.2", + "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/vue-fontawesome": "^3.0.3", "@headlessui/vue": "^1.7.16", "@heroicons/vue": "^2.0.18", + "fontawesome-free": "^1.0.4", "mitt": "^3.0.1" }, "devDependencies": { @@ -397,6 +402,60 @@ "node": ">=12" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", + "integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz", + "integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz", + "integrity": "sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz", + "integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/vue-fontawesome": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.3.tgz", + "integrity": "sha512-KCPHi9QemVXGMrfuwf3nNnNo129resAIQWut9QTAMXmXqL2ErABC6ohd2yY5Ipq0CLWNbKHk8TMdTXL/Zf3ZhA==", + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "vue": ">= 3.0.0 < 4" + } + }, "node_modules/@headlessui/vue": { "version": "1.7.16", "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.16.tgz", @@ -1080,6 +1139,11 @@ } } }, + "node_modules/fontawesome-free": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fontawesome-free/-/fontawesome-free-1.0.4.tgz", + "integrity": "sha512-7sX6Lbg2oQiClFFFFitJlKg20h3YTBON6rdmq3uGjNwDo8G6EjF2bfj2OjjcCUmf4OvZCgyHaXfW2JseqissLw==" + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", diff --git a/package.json b/package.json index 3c375a8..7f97194 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,13 @@ "vue": "^3.2.41" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.4.2", + "@fortawesome/free-regular-svg-icons": "^6.4.2", + "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/vue-fontawesome": "^3.0.3", "@headlessui/vue": "^1.7.16", "@heroicons/vue": "^2.0.18", + "fontawesome-free": "^1.0.4", "mitt": "^3.0.1" } } diff --git a/public/images/application-octet-stream.svg b/public/images/application-octet-stream.svg new file mode 100644 index 0000000..8740d02 --- /dev/null +++ b/public/images/application-octet-stream.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/application-pdf.svg b/public/images/application-pdf.svg new file mode 100644 index 0000000..25cdb4d --- /dev/null +++ b/public/images/application-pdf.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/ark.svg b/public/images/ark.svg new file mode 100644 index 0000000..4c7a28b --- /dev/null +++ b/public/images/ark.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/images/audio-x-generic.svg b/public/images/audio-x-generic.svg new file mode 100644 index 0000000..4a5d5f7 --- /dev/null +++ b/public/images/audio-x-generic.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/folder-blue.svg b/public/images/folder-blue.svg new file mode 100644 index 0000000..ce7b5b5 --- /dev/null +++ b/public/images/folder-blue.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/image-x-generic.svg b/public/images/image-x-generic.svg new file mode 100644 index 0000000..330c8f3 --- /dev/null +++ b/public/images/image-x-generic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/text-x-generic.svg b/public/images/text-x-generic.svg new file mode 100644 index 0000000..e650506 --- /dev/null +++ b/public/images/text-x-generic.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/images/video-x-generic.svg b/public/images/video-x-generic.svg new file mode 100644 index 0000000..51089a0 --- /dev/null +++ b/public/images/video-x-generic.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/images/x-office-document.svg b/public/images/x-office-document.svg new file mode 100644 index 0000000..0ba903b --- /dev/null +++ b/public/images/x-office-document.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/x-office-spreadsheet.svg b/public/images/x-office-spreadsheet.svg new file mode 100644 index 0000000..2fb1e95 --- /dev/null +++ b/public/images/x-office-spreadsheet.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/resources/css/app.css b/resources/css/app.css index 8c7915f..3862137 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -45,3 +45,7 @@ opacity: 0; } } + +.errormsg { + white-space: pre-line; +} diff --git a/resources/js/Components/NavLink.vue b/resources/js/Components/NavLink.vue index 108fdbb..54fe10e 100644 --- a/resources/js/Components/NavLink.vue +++ b/resources/js/Components/NavLink.vue @@ -14,8 +14,8 @@ const props = defineProps({ const classes = computed(() => props.active - ? "flex items-center p-3 mb-1 bg-sky-600 rounded-lg font-medium leading-5 text-slate-900 dark:text-gray-100 transition duration-150 ease-in-out hover:ring-sky-500" - : "flex items-center p-3 mb-1 rounded-lg font-medium leading-5 text-gray-500 border border-zinc-900 dark:text-gray-400 hover:text-gray-700 dark:hover:text-zinc-100 hover:border-sky-500 hover:ring-sky-500 dark:hover:border-sky-600 hover:ring-sky-500 dark:hover:ring-sky-600 transition duration-150 ease-in-out" + ? "flex items-center p-3 mb-1 bg-sky-600 rounded font-medium leading-5 text-slate-900 dark:text-gray-100 transition duration-150 ease-in-out hover:ring-sky-500" + : "flex items-center p-3 mb-1 rounded font-medium leading-5 text-gray-500 border border-zinc-900 dark:text-gray-400 hover:text-gray-700 dark:hover:text-zinc-100 hover:border-sky-500 hover:ring-sky-500 dark:hover:border-sky-600 hover:ring-sky-500 dark:hover:ring-sky-600 transition duration-150 ease-in-out" ); diff --git a/resources/js/Components/custom/DeleteButton.vue b/resources/js/Components/custom/DeleteButton.vue new file mode 100644 index 0000000..8d081d1 --- /dev/null +++ b/resources/js/Components/custom/DeleteButton.vue @@ -0,0 +1,41 @@ + + + diff --git a/resources/js/Components/custom/DownloadButton.vue b/resources/js/Components/custom/DownloadButton.vue new file mode 100644 index 0000000..29b62cf --- /dev/null +++ b/resources/js/Components/custom/DownloadButton.vue @@ -0,0 +1,128 @@ + + + diff --git a/resources/js/Components/custom/FileIcon.vue b/resources/js/Components/custom/FileIcon.vue new file mode 100644 index 0000000..032bd93 --- /dev/null +++ b/resources/js/Components/custom/FileIcon.vue @@ -0,0 +1,73 @@ + + + diff --git a/resources/js/Components/custom/FileUploadErrorModal.vue b/resources/js/Components/custom/FileUploadErrorModal.vue new file mode 100644 index 0000000..411c882 --- /dev/null +++ b/resources/js/Components/custom/FileUploadErrorModal.vue @@ -0,0 +1,40 @@ + + + diff --git a/resources/js/Components/custom/MainBreadcrumbButton.vue b/resources/js/Components/custom/MainBreadcrumbButton.vue new file mode 100644 index 0000000..0856ac2 --- /dev/null +++ b/resources/js/Components/custom/MainBreadcrumbButton.vue @@ -0,0 +1,26 @@ + + + diff --git a/resources/js/Components/custom/Navigation.vue b/resources/js/Components/custom/Navigation.vue index f3798c0..ffcf41a 100644 --- a/resources/js/Components/custom/Navigation.vue +++ b/resources/js/Components/custom/Navigation.vue @@ -26,12 +26,26 @@ import NavLink from "../NavLink.vue";
My Files - Shared with Me - Shared by Me - Recycle Bin + Shared with Me + Shared by Me + Recycle Bin
diff --git a/resources/js/Components/custom/NewDropdown.vue b/resources/js/Components/custom/NewDropdown.vue index af69558..de30702 100644 --- a/resources/js/Components/custom/NewDropdown.vue +++ b/resources/js/Components/custom/NewDropdown.vue @@ -1,24 +1,36 @@