├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitignore ├── Dockerfile ├── LICENSE ├── Readme.md ├── app ├── Exceptions │ └── PersonalDriveExceptions │ │ ├── FetchFileException.php │ │ ├── FileRenameException.php │ │ ├── ImageRelatedException.php │ │ ├── MoveFileException.php │ │ ├── PersonalDriveException.php │ │ ├── ShareFileException.php │ │ ├── ThrottleException.php │ │ ├── ThumbnailException.php │ │ ├── UUIDException.php │ │ └── UploadFileException.php ├── Helpers │ ├── DownloadHelper.php │ ├── EncryptHelper.php │ ├── FileOperationsHelper.php │ ├── FileSizeFormatter.php │ ├── ResponseHelper.php │ └── UploadFileHelper.php ├── Http │ ├── Controllers │ │ ├── AdminControllers │ │ │ ├── AdminConfigController.php │ │ │ └── SetupController.php │ │ ├── Auth │ │ │ └── AuthenticatedSessionController.php │ │ ├── Controller.php │ │ ├── DriveControllers │ │ │ ├── DownloadController.php │ │ │ ├── FetchFileController.php │ │ │ ├── FileDeleteController.php │ │ │ ├── FileManagerController.php │ │ │ ├── FileRenameController.php │ │ │ ├── FileSaveController.php │ │ │ ├── MoveFilesController.php │ │ │ ├── ReSyncController.php │ │ │ ├── SearchFilesController.php │ │ │ ├── ThumbnailController.php │ │ │ └── UploadController.php │ │ └── ShareControllers │ │ │ ├── ShareFilesGenController.php │ │ │ ├── ShareFilesGuestController.php │ │ │ ├── ShareFilesModController.php │ │ │ └── SharedListController.php │ ├── Middleware │ │ ├── CheckAdmin.php │ │ ├── CheckSetup.php │ │ ├── CleanupTempFiles.php │ │ ├── EnsureFrontendBuilt.php │ │ ├── HandleAuthOrGuestMiddleware.php │ │ ├── HandleGuestShareMiddleware.php │ │ ├── HandleInertiaMiddlware.php │ │ └── PreventSetupAccess.php │ ├── Requests │ │ ├── AdminRequests │ │ │ ├── AdminConfigRequest.php │ │ │ └── SetupAccountRequest.php │ │ ├── Auth │ │ │ └── LoginRequest.php │ │ ├── CommonRequest.php │ │ └── DriveRequests │ │ │ ├── CreateItemRequest.php │ │ │ ├── DownloadRequest.php │ │ │ ├── FetchFileRequest.php │ │ │ ├── FileDeleteRequest.php │ │ │ ├── FileManagerRequest.php │ │ │ ├── FileRenameRequest.php │ │ │ ├── FileSaveRequest.php │ │ │ ├── GenThumbnailRequest.php │ │ │ ├── MoveFilesRequest.php │ │ │ ├── ReplaceAbortRequest.php │ │ │ ├── SearchRequest.php │ │ │ ├── ShareFilesGenRequest.php │ │ │ ├── ShareFilesGuestPasswordRequest.php │ │ │ ├── ShareFilesGuestRequest.php │ │ │ ├── ShareFilesModRequest.php │ │ │ └── UploadRequest.php │ └── Resources │ │ └── AdminConfig │ │ └── AdminConfigResource.php ├── Models │ ├── LocalFile.php │ ├── Setting.php │ ├── Share.php │ ├── SharedFile.php │ └── User.php ├── Providers │ ├── AppServiceProvider.php │ └── DatabaseFileServiceProvider.php ├── Rules │ └── ValidPath.php ├── Services │ ├── AdminConfigService.php │ ├── DownloadService.php │ ├── FileDeleteService.php │ ├── FileMoveService.php │ ├── FileRenameService.php │ ├── LPathService.php │ ├── LocalFileStatsService.php │ ├── ThumbnailService.php │ ├── UUIDService.php │ └── UploadService.php └── Traits │ └── FlashMessages.php ├── artisan ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── cache.php ├── database.php ├── debugbar.php ├── filesystems.php ├── hashidable.php ├── logging.php ├── mail.php ├── queue.php ├── services.php └── session.php ├── database ├── factories │ ├── FileFactory.php │ ├── SettingFactory.php │ └── UserFactory.php └── migrations │ ├── 0001_01_01_000000_create_users_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 0001_01_01_000002_create_jobs_table.php │ ├── 2024_12_28_130626_create_settings_table.php │ ├── 2024_12_28_134837_add_default_settings.php │ ├── 2024_12_28_181917_create_local_files.php │ ├── 2025_01_02_091305_add_file_type_to_localfiles.php │ ├── 2025_01_03_134103_add_thumbnails.php │ ├── 2025_01_04_083848_add_index_to_local_files.php │ ├── 2025_01_06_132053_add_shares_table.php │ ├── 2025_01_07_085657_add_shares_table_accessed_count.php │ ├── 2025_01_08_130204_change_id_to_ulid_localfile.php │ └── 2025_01_08_195508_add_public_path_to_shares_table.php ├── docker ├── apache.conf └── entrypoint.sh ├── eslint.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── phpunit.xml ├── postcss.config.js ├── public ├── .htaccess ├── favicon.ico ├── img │ ├── img.png │ ├── list_view.png │ ├── logo.png │ ├── share-screen.png │ ├── sort.svg │ └── tile_view.png ├── index.php └── robots.txt ├── resources ├── css │ └── app.css ├── js │ ├── Components │ │ ├── ApplicationLogo.jsx │ │ ├── Checkbox.jsx │ │ ├── DangerButton.jsx │ │ ├── InputError.jsx │ │ ├── InputLabel.jsx │ │ ├── Modal.jsx │ │ ├── NavLink.jsx │ │ ├── PrimaryButton.jsx │ │ ├── ResponsiveNavLink.jsx │ │ ├── SecondaryButton.jsx │ │ └── TextInput.jsx │ ├── Contexts │ │ └── CutFilesContext.jsx │ ├── Pages │ │ ├── Admin │ │ │ ├── Config.jsx │ │ │ ├── Layouts │ │ │ │ └── SetupRaw.jsx │ │ │ └── Setup.jsx │ │ ├── Auth │ │ │ └── Login.jsx │ │ ├── Drive │ │ │ ├── Components │ │ │ │ ├── AlertBox.jsx │ │ │ │ ├── Breadcrumb.jsx │ │ │ │ ├── CreateFolderModal.jsx │ │ │ │ ├── CutButton.jsx │ │ │ │ ├── DeleteButton.jsx │ │ │ │ ├── DownloadButton.jsx │ │ │ │ ├── DropZone.jsx │ │ │ │ ├── FileBrowserSection.jsx │ │ │ │ ├── FileItem.jsx │ │ │ │ ├── FileList │ │ │ │ │ ├── FileListRow.jsx │ │ │ │ │ ├── FileTileViewCard.jsx │ │ │ │ │ ├── ImageViewer.jsx │ │ │ │ │ ├── ListView.jsx │ │ │ │ │ ├── MediaViewer.jsx │ │ │ │ │ ├── PdfViewer.jsx │ │ │ │ │ ├── RenameModal.jsx │ │ │ │ │ ├── TileViewOne.jsx │ │ │ │ │ ├── TxtViewer.jsx │ │ │ │ │ └── VideoPlayer.jsx │ │ │ │ ├── FolderItem.jsx │ │ │ │ ├── Generic │ │ │ │ │ └── Button.jsx │ │ │ │ ├── Modal.jsx │ │ │ │ ├── PasteButton.jsx │ │ │ │ ├── RefreshButton.jsx │ │ │ │ ├── ReplaceAbortModal.jsx │ │ │ │ ├── SearchBar.jsx │ │ │ │ ├── Shares │ │ │ │ │ ├── RenameModalButton.jsx │ │ │ │ │ ├── ShareModal.jsx │ │ │ │ │ └── ShowShareModalButton.jsx │ │ │ │ └── UploadMenu.jsx │ │ │ ├── DriveHome.jsx │ │ │ ├── Hooks │ │ │ │ ├── useClickOutside.jsx │ │ │ │ ├── useSearchUtil.jsx │ │ │ │ ├── useSelectionutil.jsx │ │ │ │ └── useThumbnailGenerator.jsx │ │ │ ├── Layouts │ │ │ │ ├── GuestLayout.jsx │ │ │ │ └── Header.jsx │ │ │ ├── ShareFilesGuestHome.jsx │ │ │ ├── Shares │ │ │ │ ├── AllShares.jsx │ │ │ │ └── CheckSharePassword.jsx │ │ │ └── Svgs │ │ │ │ └── SortIcon.jsx │ │ ├── Landing.jsx │ │ └── Rejected.jsx │ ├── app.jsx │ └── bootstrap.js └── views │ └── app.blade.php ├── routes ├── auth.php ├── console.php └── web.php ├── setup.sh ├── storage ├── app │ ├── .gitignore │ ├── private │ │ └── .gitignore │ └── public │ │ └── .gitignore ├── debugbar │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.js ├── tests ├── Feature │ ├── Auth │ │ └── AuthenticationTest.php │ ├── Helpers │ │ ├── FileSizeFormatterTest.php │ │ ├── ResponseHelperTest.php │ │ └── UploadFileHelperTest.php │ ├── Services │ │ └── UUIDServiceTest.php │ └── Traits │ │ └── FlashMessagesTest.php ├── Pest.php ├── TestCase.php └── Unit │ ├── ExampleTest.php │ ├── Services │ ├── DownloadServiceTest.php │ ├── FileDeleteServiceTest.php │ └── LPathServiceTest.php │ └── Validation │ └── CommonRequestTest.php └── vite.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /storage/pail 8 | /vendor 9 | /database/db/database.sqlite 10 | /storage/logs 11 | /node_modules 12 | .env 13 | /.idea 14 | /.nova 15 | /.vscode 16 | ./database/db/database.sqlite -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Personal_Drive 2 | APP_ENV=production 3 | APP_KEY= 4 | APP_DEBUG=false 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://localhost 7 | 8 | APP_LOCALE=en 9 | APP_FALLBACK_LOCALE=en 10 | APP_FAKER_LOCALE=en_US 11 | 12 | APP_MAINTENANCE_DRIVER=file 13 | # APP_MAINTENANCE_STORE=database 14 | 15 | PHP_CLI_SERVER_WORKERS=4 16 | 17 | BCRYPT_ROUNDS=12 18 | 19 | LOG_CHANNEL=stack 20 | LOG_STACK=single 21 | LOG_DEPRECATIONS_CHANNEL=null 22 | LOG_LEVEL=debug 23 | 24 | DB_CONNECTION=sqlite 25 | # DB_HOST=127.0.0.1 26 | # DB_PORT=3306 27 | # DB_DATABASE=laravel 28 | # DB_USERNAME=root 29 | # DB_PASSWORD= 30 | 31 | SESSION_DRIVER=database 32 | SESSION_LIFETIME=120 33 | SESSION_ENCRYPT=false 34 | SESSION_PATH=/ 35 | SESSION_DOMAIN=null 36 | 37 | BROADCAST_CONNECTION=log 38 | FILESYSTEM_DISK=local 39 | QUEUE_CONNECTION=database 40 | 41 | CACHE_STORE=database 42 | CACHE_PREFIX= 43 | 44 | MEMCACHED_HOST=127.0.0.1 45 | 46 | REDIS_CLIENT=phpredis 47 | REDIS_HOST=127.0.0.1 48 | REDIS_PASSWORD=null 49 | REDIS_PORT=6379 50 | 51 | MAIL_MAILER=log 52 | MAIL_HOST=127.0.0.1 53 | MAIL_PORT=2525 54 | MAIL_USERNAME=null 55 | MAIL_PASSWORD=null 56 | MAIL_ENCRYPTION=null 57 | MAIL_FROM_ADDRESS="hello@example.com" 58 | MAIL_FROM_NAME="${APP_NAME}" 59 | 60 | AWS_ACCESS_KEY_ID= 61 | AWS_SECRET_ACCESS_KEY= 62 | AWS_DEFAULT_REGION=us-east-1 63 | AWS_BUCKET= 64 | AWS_USE_PATH_STYLE_ENDPOINT=false 65 | 66 | VITE_APP_NAME="${APP_NAME}" 67 | 68 | DISABLE_HTTPS=false 69 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /storage/pail 8 | /vendor 9 | .env 10 | .env.backup 11 | .env.production 12 | .phpactor.json 13 | .phpunit.result.cache 14 | Homestead.json 15 | Homestead.yaml 16 | auth.json 17 | npm-debug.log 18 | yarn-error.log 19 | /.fleet 20 | /.idea 21 | /.nova 22 | /.vscode 23 | /.zed 24 | database/db/database.sqlite 25 | ./database/db/database.sqlite 26 | -------------------------------------------------------------------------------- /app/Exceptions/PersonalDriveExceptions/FetchFileException.php: -------------------------------------------------------------------------------- 1 | open($outputZipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { 21 | throw FetchFileException::couldNotZip(); 22 | } 23 | 24 | foreach ($localFiles as $localFile) { 25 | $pathName = $localFile->getPrivatePathNameForFile(); 26 | if (! file_exists($pathName)) { 27 | continue; 28 | } 29 | 30 | if (is_dir($pathName)) { 31 | $iterator = new RecursiveIteratorIterator( 32 | new RecursiveDirectoryIterator($pathName), 33 | RecursiveIteratorIterator::SELF_FIRST 34 | ); 35 | 36 | foreach ($iterator as $file) { 37 | if ($file->isDir()) { 38 | continue; 39 | } 40 | 41 | $filePath = $file->getRealPath(); 42 | $relativePath = substr($filePath, strlen(dirname($pathName)) + 1); 43 | 44 | $zip->addFile($filePath, $relativePath); 45 | } 46 | } else { 47 | $zip->addFile($pathName, basename($pathName)); 48 | } 49 | } 50 | 51 | $zip->close(); 52 | 53 | return $zip; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Helpers/EncryptHelper.php: -------------------------------------------------------------------------------- 1 | pathService = $pathService; 17 | 18 | $basePath = $this->pathService->getStorageDirPath(); 19 | $adapter = new LocalFilesystemAdapter($basePath); 20 | $this->filesystem = new Filesystem($adapter); 21 | } 22 | 23 | public function move(string $src, string $dest) 24 | { 25 | return $this->filesystem->move($src, $dest); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/Helpers/FileSizeFormatter.php: -------------------------------------------------------------------------------- 1 | = 1024; $i++) { 15 | $bytes /= 1024; 16 | } 17 | 18 | return round($bytes, ($i < 2 ? 0 : 2)).' '.$units[$i]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Helpers/ResponseHelper.php: -------------------------------------------------------------------------------- 1 | json([ 12 | 'status' => $status, 13 | 'message' => $message, 14 | ]); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Helpers/UploadFileHelper.php: -------------------------------------------------------------------------------- 1 | coz different environments 13 | $fullPath = ltrim($_FILES['files']['full_path'][$fileIndex], '.'); 14 | return self::sanitizePath($fullPath); 15 | 16 | } 17 | private static function sanitizePath(string $path): string 18 | { 19 | if (str_contains($path, '..')) { 20 | UploadFileException::invalidPath(); 21 | } 22 | return $path; 23 | } 24 | 25 | public static function makeFolder(string $path, int $permission = 0750): bool 26 | { 27 | if (file_exists($path)) { 28 | throw UploadFileException::nonewdir('folder'); 29 | } 30 | if (! mkdir($path, $permission, true) && !is_dir($path)) { 31 | return false; 32 | } 33 | 34 | return true; 35 | } 36 | 37 | public static function makeFile(string $path, int $permission = 0750): bool 38 | { 39 | if (file_exists($path) && is_file($path)) { 40 | return true; 41 | } 42 | if (file_put_contents($path, '') === false && is_file($path) === false) { 43 | return false; 44 | } 45 | 46 | return true; 47 | } 48 | public static function deleteFolder(string $dir): bool 49 | { 50 | 51 | if (File::exists($dir)) { 52 | return File::deleteDirectory($dir); // Delete everything inside UUID dir 53 | } 54 | 55 | return true; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /app/Http/Controllers/AdminControllers/AdminConfigController.php: -------------------------------------------------------------------------------- 1 | adminConfigService = $adminConfigService; 25 | $this->localFileStatsService = $localFileStatsService; 26 | } 27 | 28 | public function index(Request $request): Response 29 | { 30 | $setupMode = (bool) $request->query('setupMode'); 31 | $storagePath = Setting::getSettingByKeyName(Setting::$storagePath); 32 | 33 | return Inertia::render('Admin/Config', [ 34 | 'storage_path' => $storagePath, 35 | 'php_max_upload_size' => $this->adminConfigService->getPhpUploadMaxFilesize(), 36 | 'php_post_max_size' => $this->adminConfigService->getPhpPostMaxSize(), 37 | 'php_max_file_uploads' => $this->adminConfigService->getPhpMaxFileUploads(), 38 | 'setupMode' => $setupMode, 39 | ]); 40 | } 41 | 42 | public function update(AdminConfigRequest $request): RedirectResponse 43 | { 44 | $storagePath = $request->validated('storage_path'); 45 | $storagePath = trim(rtrim($storagePath, '/')); 46 | $updateStoragePathRes = $this->adminConfigService->updateStoragePath($storagePath); 47 | session()->flash('message', $updateStoragePathRes['message']); 48 | session()->flash('status', $updateStoragePathRes['status']); 49 | if ($updateStoragePathRes['status']) { 50 | LocalFile::clearTable(); 51 | $this->localFileStatsService->generateStats(); 52 | 53 | return redirect()->route('drive'); 54 | } 55 | 56 | return redirect()->back(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Http/Controllers/AdminControllers/SetupController.php: -------------------------------------------------------------------------------- 1 | true]); 24 | $status = false; 25 | $message = 'Error. could not create user. Try re-installing, checking permissions for storage folder'; 26 | if ($user = User::create([ 27 | 'username' => $request->username, 28 | 'is_admin' => 1, 29 | 'password' => bcrypt($request->password), 30 | ]) 31 | ) { 32 | $message = 'Created User successfully'; 33 | $status = true; 34 | $request->session()->invalidate(); 35 | config(['session.driver' => 'database']); 36 | Auth::login($user, true); 37 | $request->session()->regenerate(); 38 | } 39 | session()->flash('status', $status); 40 | session()->flash('message', $message); 41 | 42 | return redirect()->route('admin-config', ['setupMode' => true]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | authenticate(); 29 | 30 | $request->session()->regenerate(); 31 | 32 | return redirect(route('drive', absolute: false)); 33 | } 34 | 35 | /** 36 | * Destroy an authenticated session. 37 | */ 38 | public function destroy(Request $request): RedirectResponse 39 | { 40 | Auth::guard('web')->logout(); 41 | 42 | $request->session()->invalidate(); 43 | 44 | $request->session()->regenerateToken(); 45 | 46 | return redirect('/login'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | pathService = $pathService; 27 | $this->downloadService = $downloadService; 28 | } 29 | 30 | public function index(DownloadRequest $request): BinaryFileResponse|JsonResponse 31 | { 32 | $fileKeyArray = $request->validated('fileList'); 33 | $localFiles = LocalFile::getByIds($fileKeyArray)->get(); 34 | if (! $localFiles || count($localFiles) === 0) { 35 | throw FetchFileException::notFoundDownload(); 36 | } 37 | try { 38 | $downloadFilePath = $this->downloadService->generateDownloadPath($localFiles); 39 | if (! file_exists($downloadFilePath)) { 40 | return ResponseHelper::json('Perhaps trying to download empty dir ? ', false); 41 | } 42 | 43 | return $this->getDownloadResponse($downloadFilePath); 44 | } catch (Exception $e) { 45 | return ResponseHelper::json($e->getMessage(), false); 46 | } 47 | } 48 | 49 | private function getDownloadResponse(string $downloadFilePath): BinaryFileResponse 50 | { 51 | return Response::download( 52 | $downloadFilePath, 53 | basename($downloadFilePath), 54 | ['Content-Disposition' => 'attachment; filename="'.basename($downloadFilePath).'"'] 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/Http/Controllers/DriveControllers/FetchFileController.php: -------------------------------------------------------------------------------- 1 | localFileStatsService = $localFileStatsService; 27 | $this->thumbnailService = $thumbnailService; 28 | } 29 | 30 | /** 31 | * @throws FetchFileException 32 | */ 33 | public function index(FetchFileRequest $request): void 34 | { 35 | $file = $this->handleHashRequest($request); 36 | $filePrivatePathName = $file->getPrivatePathNameForFile(); 37 | if ($file->file_type === 'text') { 38 | response()->stream(function () use ($filePrivatePathName) { 39 | readfile($filePrivatePathName); 40 | }, 200, [ 41 | 'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0', 42 | 'Pragma' => 'no-cache', 43 | 'Content-Type' => 'text/plain', 44 | ])->send(); 45 | } else { 46 | VideoStreamer::streamFile($filePrivatePathName); 47 | } 48 | } 49 | 50 | /** 51 | * @throws FetchFileException 52 | */ 53 | public function getThumb(FetchFileRequest $request): void 54 | { 55 | $file = $this->handleHashRequest($request); 56 | if (! $file->has_thumbnail) { 57 | throw FetchFileException::notFoundStream(); 58 | } 59 | $filePrivatePathName = $this->thumbnailService->getFullFileThumbnailPath($file); 60 | if (file_exists($filePrivatePathName)) { 61 | VideoStreamer::streamFile($filePrivatePathName); 62 | } 63 | } 64 | 65 | /** 66 | * @throws FetchFileException 67 | */ 68 | private function handleHashRequest(FetchFileRequest $request): LocalFile 69 | { 70 | $fileId = $request->validated('id'); 71 | 72 | $file = LocalFile::find($fileId); 73 | if (! $file || ! $file->file_type) { 74 | throw FetchFileException::notFoundStream(); 75 | } 76 | 77 | return $file; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/Http/Controllers/DriveControllers/FileDeleteController.php: -------------------------------------------------------------------------------- 1 | localFileStatsService = $localFileStatsService; 29 | $this->pathService = $pathService; 30 | $this->fileDeleteService = $fileDeleteService; 31 | } 32 | 33 | public function deleteFiles(FileDeleteRequest $request): RedirectResponse 34 | { 35 | $fileKeyArray = $request->validated('fileList'); 36 | $rootPath = $this->pathService->getStorageDirPath(); 37 | $localFiles = LocalFile::getByIds($fileKeyArray); 38 | if (! $localFiles->count()) { 39 | return $this->error('No valid files in database. Try a ReSync first'); 40 | } 41 | 42 | $filesDeleted = $this->fileDeleteService->deleteFiles($localFiles, $rootPath); 43 | 44 | // delete files from database 45 | $response = $localFiles->delete(); 46 | 47 | if (! $response || ! $filesDeleted) { 48 | return $this->error('Could not delete files'); 49 | } 50 | 51 | return $this->success('Deleted '.$filesDeleted.' files'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Http/Controllers/DriveControllers/FileManagerController.php: -------------------------------------------------------------------------------- 1 | validated('path') ?? ''; 16 | 17 | $files = LocalFile::getFilesForPublicPath($path); 18 | 19 | return Inertia::render('Drive/DriveHome', [ 20 | 'files' => $files, 21 | 'path' => '/drive'.($path ? '/'.$path : ''), 22 | 'token' => csrf_token(), 23 | ]); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Controllers/DriveControllers/FileRenameController.php: -------------------------------------------------------------------------------- 1 | fileRenameService = $fileRenameService; 22 | } 23 | 24 | public function index(FileRenameRequest $request): RedirectResponse 25 | { 26 | $id = $request->validated('id'); 27 | $filename = $request->validated('filename'); 28 | 29 | $file = LocalFile::getById($id); 30 | if (!$file || !$file->getPrivatePathNameForFile()) { 31 | return $this->error('Could not find file!'); 32 | } 33 | try { 34 | $this->fileRenameService->renameFile($file, $filename); 35 | } catch (\Exception $e) { 36 | return $this->error('Could not rename file. File with same name exists? Also Check permissions. '. $e->getMessage()); 37 | } 38 | 39 | return $this->success('Renamed to '. $filename); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/Http/Controllers/DriveControllers/FileSaveController.php: -------------------------------------------------------------------------------- 1 | pathService = $pathService; 29 | $this->downloadService = $downloadService; 30 | $this->localFileStatsService = $localFileStatsService; 31 | } 32 | 33 | public function update(FileSaveRequest $request): BinaryFileResponse|JsonResponse 34 | { 35 | $id = $request->validated('id'); 36 | $content = $request->validated('content'); 37 | $localFile = LocalFile::getById($id); 38 | if (!$localFile) { 39 | return ResponseHelper::json('Could not find file ', false); 40 | } 41 | if ($localFile->file_type !== 'text' && $localFile->file_type !== 'empty') { 42 | return ResponseHelper::json('File is not a text file', false); 43 | } 44 | 45 | $privatePathFile = $localFile->getPrivatePathNameForFile(); 46 | if (!$privatePathFile) { 47 | return ResponseHelper::json('Could not find file', false); 48 | } 49 | try { 50 | file_put_contents($privatePathFile, $content); 51 | $file = new \SplFileInfo($privatePathFile); 52 | $this->localFileStatsService->updateFileStats($localFile, $file); 53 | 54 | return ResponseHelper::json('File saved successfully', true); 55 | } catch (Exception $e) { 56 | return ResponseHelper::json($e->getMessage(), false); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Http/Controllers/DriveControllers/MoveFilesController.php: -------------------------------------------------------------------------------- 1 | fileMoveService = $fileMoveService; 19 | } 20 | 21 | public function update(MoveFilesRequest $request) 22 | { 23 | 24 | $fileKeyArray = $request->validated('fileList'); 25 | $destinationPath = $request->validated('path'); 26 | try { 27 | $res = $this->fileMoveService->moveFiles($fileKeyArray, $destinationPath); 28 | } catch (\Exception $e) { 29 | return $this->error('Error: Could not move files'); 30 | } 31 | 32 | return $res ? $this->success('Files moved successfully.') : $this->error('Error: Could not move files'); 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Controllers/DriveControllers/ReSyncController.php: -------------------------------------------------------------------------------- 1 | localFileStatsService = $localFileStatsService; 21 | } 22 | 23 | public function index(): RedirectResponse 24 | { 25 | LocalFile::clearTable(); 26 | $filesUpdated = $this->localFileStatsService->generateStats(); 27 | if ($filesUpdated > 0) { 28 | return $this->success('Sync successful. Found : '.$filesUpdated.' files'); 29 | } 30 | 31 | return $this->error('No files found !'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Controllers/DriveControllers/SearchFilesController.php: -------------------------------------------------------------------------------- 1 | validated('query') ?? '/'; 16 | 17 | $files = LocalFile::searchFiles($searchQuery); 18 | 19 | return Inertia::render('Drive/DriveHome', [ 20 | 'files' => $files, 21 | 'searchResults' => true, 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Controllers/DriveControllers/ThumbnailController.php: -------------------------------------------------------------------------------- 1 | thumbnailService = $thumbnailService; 21 | } 22 | 23 | public function update(GenThumbnailRequest $request): RedirectResponse 24 | { 25 | $fileIds = $request->validated('ids'); 26 | $publicPath = $request->validated('path') ?? ''; 27 | $publicPath = preg_replace('#^/drive/?#', '', $publicPath); 28 | if (!$fileIds) { 29 | session()->flash('message', 'Could not generate thumbnails'); 30 | } 31 | LocalFile::setHasThumbnail($fileIds); 32 | $thumbsGenerated = $this->thumbnailService->genThumbnailsForFileIds($fileIds); 33 | if ($thumbsGenerated === 0) { 34 | session()->flash('message', 'No thumbnails generated. No valid files found'); 35 | } 36 | return redirect()->route('drive', ['path' => $publicPath]); 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Http/Controllers/ShareControllers/ShareFilesGenController.php: -------------------------------------------------------------------------------- 1 | localFileStatsService = $localFileStatsService; 30 | $this->pathService = $pathService; 31 | } 32 | 33 | public function index(ShareFilesGenRequest $request): RedirectResponse 34 | { 35 | $fileKeyArray = $request->validated('fileList'); 36 | $slug = $request->validated('slug'); 37 | $password = $request->validated('password'); 38 | $expiry = $request->validated('expiry'); 39 | $localFiles = LocalFile::getByIds($fileKeyArray)->get(); 40 | 41 | $slug = $slug ?: Str::random(10); 42 | 43 | if (! $localFiles->count()) { 44 | throw ShareFileException::couldNotShare(); 45 | } 46 | $hashedPassword = $password ? Hash::make($password) : null; 47 | 48 | $share = Share::add($slug, $hashedPassword, $expiry, $localFiles[0]->public_path); 49 | 50 | if (! $share) { 51 | throw ShareFileException::couldNotShare(); 52 | } 53 | 54 | $sharedFiles = SharedFile::addArray($localFiles, $share->id); 55 | if (! $sharedFiles) { 56 | throw ShareFileException::couldNotShare(); 57 | } 58 | 59 | $sharedLink = url('/').'/shared/'.$slug; 60 | 61 | return redirect()->back()->with('shared_link', $sharedLink); 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Http/Controllers/ShareControllers/ShareFilesGuestController.php: -------------------------------------------------------------------------------- 1 | validated('slug'); 21 | $path = $request->validated('path'); 22 | $share = Share::whereBySlug($slug)->first(); 23 | 24 | if (! $share) { 25 | throw ShareFileException::couldNotShare(); 26 | } 27 | 28 | if ($path) { 29 | $files = Share::getFilenamesByPath($share->id, ($share->public_path ? $share->public_path.DIRECTORY_SEPARATOR : '').$path); 30 | } else { 31 | $files = Share::getFilenamesBySlug($slug); 32 | } 33 | $files = LocalFile::modifyFileCollectionForGuest($files, $share->public_path); 34 | 35 | return Inertia::render('Drive/ShareFilesGuestHome', [ 36 | 'files' => $files, 37 | 'path' => '/shared/'.$slug.($path ? '/'.$path : ''), 38 | 'token' => csrf_token(), 39 | 'guest' => 'on', 40 | 'slug' => $slug, 41 | ]); 42 | } 43 | 44 | public function passwordPage(ShareFilesGuestRequest $request): Response 45 | { 46 | $slug = $request->validated('slug'); 47 | 48 | return Inertia('Drive/Shares/CheckSharePassword', ['slug' => $slug]); 49 | } 50 | 51 | public function checkPassword(ShareFilesGuestPasswordRequest $request): RedirectResponse 52 | { 53 | $slug = $request->validated('slug'); 54 | $password = $request->validated('password'); 55 | $share = Share::whereBySlug($slug)->first(); 56 | 57 | if (! $share) { 58 | throw ShareFileException::shareWrongPassword(); 59 | // Commented below. We do not want attacker to know share does not exist 60 | // throw ShareFileException::couldNotShare(); 61 | } 62 | 63 | if (Hash::check($password, $share->password)) { 64 | Session::put("shared_{$slug}_authenticated", true); 65 | return redirect("/shared/$slug"); 66 | } 67 | 68 | throw ShareFileException::shareWrongPassword(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/Http/Controllers/ShareControllers/ShareFilesModController.php: -------------------------------------------------------------------------------- 1 | validated('id'); 17 | if (Share::whereById($shareId)->delete()) { 18 | return $this->success('Successfully deleted share'); 19 | } 20 | 21 | return $this->error('Error! could not delete share'); 22 | } 23 | 24 | public function pause(ShareFilesModRequest $request): RedirectResponse 25 | { 26 | $shareId = $request->validated('id'); 27 | $share = Share::whereById($shareId)->first(); 28 | 29 | if (! $share) { 30 | return $this->error('Error! could not find share'); 31 | } 32 | 33 | $update = Share::whereById($shareId)->update(['enabled' => ! $share->enabled]); 34 | if ($update) { 35 | return $this->success('Paused'); 36 | } 37 | 38 | return $this->error('Error! could not pause share'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Http/Controllers/ShareControllers/SharedListController.php: -------------------------------------------------------------------------------- 1 | localFileStatsService = $localFileStatsService; 25 | $this->pathService = $pathService; 26 | } 27 | 28 | public function index(): Response 29 | { 30 | $shares = Share::getAllUnExpired(); 31 | 32 | return Inertia::render('Drive/Shares/AllShares', ['shares' => $shares]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Http/Middleware/CheckAdmin.php: -------------------------------------------------------------------------------- 1 | is_admin) { 14 | return $next($request); 15 | } 16 | return redirect()->route('rejected', ['message' => 'You do not have admin access']); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Middleware/CheckSetup.php: -------------------------------------------------------------------------------- 1 | count() === 0) && ! $request->is('setup*', 'error') 17 | ) { 18 | config(['session.driver' => 'array']); 19 | return redirect('/setup/account'); 20 | } 21 | 22 | return $next($request); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Middleware/CleanupTempFiles.php: -------------------------------------------------------------------------------- 1 | uploadService = $uploadService; 17 | } 18 | 19 | public function handle(Request $request, Closure $next): Response 20 | { 21 | $this->uploadService->cleanOldTempFiles(); 22 | 23 | return $next($request); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Http/Middleware/EnsureFrontendBuilt.php: -------------------------------------------------------------------------------- 1 | 'Frontend not built. Ensure node, npm are installed Run "npm install && npm run build"']); 15 | } 16 | 17 | return $next($request); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Middleware/HandleAuthOrGuestMiddleware.php: -------------------------------------------------------------------------------- 1 | handle($request, $next); 18 | } catch (AuthenticationException $e) { 19 | // If not authenticated, delegate to the HandleGuestShareRequests middleware 20 | return app(HandleGuestShareMiddleware::class)->handle($request, $next); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Middleware/HandleGuestShareMiddleware.php: -------------------------------------------------------------------------------- 1 | route('slug') ?? $request->input('slug'); 16 | if (!$slug) { 17 | return redirect()->route('rejected'); 18 | } 19 | $share = Share::whereBySlug($slug)->first(); 20 | 21 | if (! $share || ! $share->enabled || ($share->expiry && $share->created_at->addDays($share->expiry)->lt(now()))) { 22 | 23 | return redirect()->route('login', ['slug' => $slug]); 24 | } 25 | if ($this->isNeedsPassword($share, $slug)) { 26 | return redirect()->route('shared.password', ['slug' => $slug]); 27 | } 28 | 29 | return $next($request); 30 | } 31 | 32 | public function isNeedsPassword($share, mixed $slug): bool 33 | { 34 | return $share->password && ! Session::get("shared_{$slug}_authenticated"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Middleware/HandleInertiaMiddlware.php: -------------------------------------------------------------------------------- 1 | $request->session()->get('message'), 32 | 'status' => $request->session()->get('status'), 33 | ]; 34 | $sharedLink = $request->session()->get('shared_link'); 35 | if ($sharedLink) { 36 | $flashA['shared_link'] = $sharedLink; 37 | } 38 | 39 | $moreInfo = $request->session()->get('more_info'); 40 | if ($moreInfo) { 41 | $flashA['more_info'] = $moreInfo; 42 | } 43 | 44 | return [ 45 | ...parent::share($request), 46 | 'flash' => $flashA, 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventSetupAccess.php: -------------------------------------------------------------------------------- 1 | count() > 0) { 16 | return redirect('/')->with('message', 'Setup already completed.'); 17 | } 18 | 19 | return $next($request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Requests/AdminRequests/AdminConfigRequest.php: -------------------------------------------------------------------------------- 1 | CommonRequest::pathRules(), 14 | ]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Http/Requests/AdminRequests/SetupAccountRequest.php: -------------------------------------------------------------------------------- 1 | CommonRequest::usernameRules(), 14 | 'password' => CommonRequest::passwordRules(), 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Requests/Auth/LoginRequest.php: -------------------------------------------------------------------------------- 1 | |string> 28 | */ 29 | public function rules(): array 30 | { 31 | return [ 32 | 'username' => CommonRequest::usernameRules(), 33 | 'password' => CommonRequest::passwordRules(), 34 | ]; 35 | } 36 | 37 | /** 38 | * Attempt to authenticate the request's credentials. 39 | * 40 | * @throws ValidationException 41 | */ 42 | public function authenticate(): void 43 | { 44 | $this->ensureIsNotRateLimited(); 45 | 46 | if (! Auth::attempt($this->only('username', 'password'), $this->boolean('remember'))) { 47 | RateLimiter::hit($this->throttleKey()); 48 | 49 | throw ValidationException::withMessages([ 50 | 'username' => trans('auth.failed'), 51 | ]); 52 | } 53 | 54 | RateLimiter::clear($this->throttleKey()); 55 | } 56 | 57 | /** 58 | * Ensure the login request is not rate limited. 59 | * 60 | * @throws ValidationException 61 | */ 62 | public function ensureIsNotRateLimited(): void 63 | { 64 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { 65 | return; 66 | } 67 | 68 | event(new Lockout($this)); 69 | 70 | $seconds = RateLimiter::availableIn($this->throttleKey()); 71 | 72 | throw ValidationException::withMessages([ 73 | 'email' => trans('auth.throttle', [ 74 | 'seconds' => $seconds, 75 | 'minutes' => ceil($seconds / 60), 76 | ]), 77 | ]); 78 | } 79 | 80 | /** 81 | * Get the rate limiting throttle key for the request. 82 | */ 83 | public function throttleKey(): string 84 | { 85 | return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/Http/Requests/CommonRequest.php: -------------------------------------------------------------------------------- 1 | 'required|array', 35 | 'fileList.*' => 'ulid', 36 | ]; 37 | } 38 | 39 | public static function usernameRules(): array 40 | { 41 | return ['required', 'string', 'regex:/^[0-9a-z\_]+$/']; 42 | } 43 | 44 | public static function itemNameRule(): array 45 | { 46 | return ['required', 'string','max:255','regex:/^[a-zA-Z0-9_\- \.]+$/']; 47 | } 48 | 49 | public static function localFileIdRules(): array 50 | { 51 | return ['required', 'string', 'ulid']; 52 | } 53 | 54 | public static function sharePasswordRules(): array 55 | { 56 | return ['nullable', 'string', Password::min(6)]; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/CreateItemRequest.php: -------------------------------------------------------------------------------- 1 | CommonRequest::itemNameRule(), 14 | 'path' => CommonRequest::pathRules(), 15 | 'isFile' => 'boolean', 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/DownloadRequest.php: -------------------------------------------------------------------------------- 1 | CommonRequest::localFileIdRules(), 14 | ]; 15 | } 16 | 17 | protected function prepareForValidation(): void 18 | { 19 | $this->merge([ 20 | 'id' => $this->route('id'), 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/FileDeleteRequest.php: -------------------------------------------------------------------------------- 1 | ['nullable', ...CommonRequest::pathRules()]]; 13 | } 14 | 15 | protected function prepareForValidation(): void 16 | { 17 | $this->merge([ 18 | 'path' => $this->route('path'), 19 | ]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/FileRenameRequest.php: -------------------------------------------------------------------------------- 1 | CommonRequest::localFileIdRules(), 14 | 'filename' => CommonRequest::itemNameRule() 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/FileSaveRequest.php: -------------------------------------------------------------------------------- 1 | CommonRequest::localFileIdRules(), 14 | 'content' => 'required|string|max:1000000', 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/GenThumbnailRequest.php: -------------------------------------------------------------------------------- 1 | 'required|array|min:1', 14 | 'ids.*' => CommonRequest::localFileIdRules(), 15 | 'path' => CommonRequest::pathRules() 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/MoveFilesRequest.php: -------------------------------------------------------------------------------- 1 | CommonRequest::pathRules()]); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/ReplaceAbortRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string', 13 | ]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/SearchRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string|alpha_num|max:20', 13 | ]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/ShareFilesGenRequest.php: -------------------------------------------------------------------------------- 1 | ['nullable', 'unique:shares', 'string', CommonRequest::slugRegex()], 14 | 'password' => CommonRequest::sharePasswordRules(), 15 | 'expiry' => 'nullable|integer', 16 | ]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/ShareFilesGuestPasswordRequest.php: -------------------------------------------------------------------------------- 1 | CommonRequest::slugRules(), 14 | 'password' => CommonRequest::sharePasswordRules(), 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/ShareFilesGuestRequest.php: -------------------------------------------------------------------------------- 1 | CommonRequest::slugRules(), 14 | 'path' => CommonRequest::pathRules() 15 | ]; 16 | } 17 | 18 | protected function prepareForValidation(): void 19 | { 20 | $this->merge([ 21 | 'slug' => $this->input('slug', '') ?: $this->route('slug', ''), 22 | 'path' => $this->input('path', '') ?: $this->route('path', ''), 23 | ]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/ShareFilesModRequest.php: -------------------------------------------------------------------------------- 1 | 'required|integer', 13 | ]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Http/Requests/DriveRequests/UploadRequest.php: -------------------------------------------------------------------------------- 1 | 'required|array', 14 | 'files.*' => 'required|file', 15 | 'path' => CommonRequest::pathRules() 16 | ]; 17 | } 18 | 19 | public function messages(): array 20 | { 21 | return [ 22 | 'uploaded' => 'The :attribute failed to upload. Check settings. Configure upload limits.', 23 | 'files.*.uploaded' => 'The :attribute failed to upload. Check settings. Configure upload limits', 24 | ]; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Resources/AdminConfig/AdminConfigResource.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function toArray(Request $request): array 16 | { 17 | return parent::toArray($request); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Models/Setting.php: -------------------------------------------------------------------------------- 1 | $key], 19 | ['value' => $value] 20 | ); 21 | 22 | return $result->wasRecentlyCreated || $result->wasChanged() || $result->exists; 23 | } 24 | 25 | public static function getSettingByKeyName(string $key): string 26 | { 27 | $setting = static::where('key', $key)->first(); 28 | 29 | return $setting ? $setting->value : ''; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Models/Share.php: -------------------------------------------------------------------------------- 1 | $slug, 26 | 'password' => $password, 27 | 'expiry' => $expiry, 28 | 'public_path' => $publicPath, 29 | ]); 30 | } 31 | 32 | public static function getAllUnExpired(): Collection 33 | { 34 | return static::with(['sharedFiles.localFile:id,filename']) 35 | ->where(function ($query) { 36 | $query->whereRaw("datetime(created_at, '+' || expiry || ' days') > datetime('now')") 37 | ->orWhereNull('expiry'); 38 | }) 39 | ->orderBy('created_at', 'desc') 40 | ->get(); 41 | } 42 | 43 | public static function whereBySlug(string $slug): Builder 44 | { 45 | return Share::where('slug', $slug); 46 | } 47 | 48 | public static function whereById(int $id): Builder 49 | { 50 | return Share::where('id', $id); 51 | } 52 | 53 | public static function getFilenamesBySlug(string $slug): Collection 54 | { 55 | return Share::where('slug', $slug) 56 | ->firstOrFail() 57 | ->localFiles() 58 | ->get(); 59 | } 60 | 61 | public function localFiles(): HasManyThrough 62 | { 63 | return $this->hasManyThrough(LocalFile::class, SharedFile::class, 'share_id', 'id', 'id', 'file_id'); 64 | } 65 | 66 | public static function getFilenamesByPath(int $shareID, string $path) 67 | { 68 | return LocalFile::where('public_path', $path) 69 | ->whereExists(function ($query) use ($shareID) { 70 | $query->select(DB::raw(1)) 71 | ->from('local_files AS l') 72 | ->join('shared_files AS sf', 'l.id', '=', 'sf.file_id') 73 | ->join('shares AS s', 'sf.share_id', '=', 's.id') 74 | ->where('s.id', $shareID) 75 | ->whereRaw("local_files.public_path LIKE (l.public_path || '%')") 76 | ->limit(1); 77 | }) 78 | ->get(); 79 | } 80 | 81 | public function getExpiryTimeAttribute(): string 82 | { 83 | return $this->created_at->addDays($this->expiry)->format('jS M Y g:i A'); 84 | } 85 | 86 | public function sharedFiles(): HasMany 87 | { 88 | return $this->hasMany(SharedFile::class); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/Models/SharedFile.php: -------------------------------------------------------------------------------- 1 | belongsTo(Share::class); 17 | } 18 | 19 | public function localFile(): BelongsTo 20 | { 21 | return $this->belongsTo(LocalFile::class, 'file_id'); 22 | } 23 | 24 | public static function addArray(Collection $localFiles, int $shareId): bool 25 | { 26 | $sharedFiles = $localFiles->map(fn ($localFile) => self::getFileIds($shareId, $localFile))->toArray(); 27 | 28 | return SharedFile::insert($sharedFiles); 29 | } 30 | 31 | private static function getFileIds(int $shareId, LocalFile $file): array 32 | { 33 | return [ 34 | 'share_id' => $shareId, 35 | 'file_id' => $file->id, 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | */ 13 | use HasFactory; 14 | use Notifiable; 15 | 16 | /** 17 | * The attributes that are mass assignable. 18 | * 19 | * @var array 20 | */ 21 | protected $fillable = [ 22 | 'username', 23 | 'is_admin', 24 | 'password', 25 | ]; 26 | 27 | /** 28 | * The attributes that should be hidden for serialization. 29 | * 30 | * @var array 31 | */ 32 | protected $hidden = [ 33 | 'password', 34 | 'remember_token', 35 | ]; 36 | 37 | /** 38 | * Get the attributes that should be cast. 39 | * 40 | * @return array 41 | */ 42 | protected function casts(): array 43 | { 44 | return [ 45 | 'email_verified_at' => 'datetime', 46 | 'password' => 'hashed', 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(UUIDService::class, function () { 25 | $setting = $this->app->make(Setting::class); 26 | return new UUIDService($setting); 27 | }); 28 | } 29 | 30 | /** 31 | * Bootstrap any application services. 32 | */ 33 | public function boot(): void 34 | { 35 | URL::forceScheme('http'); 36 | 37 | if (config('app.env') === 'production' && !env('DISABLE_HTTPS')) { 38 | URL::forceScheme('https'); 39 | } 40 | try { 41 | if (!Schema::hasTable('sessions')) { 42 | config(['session.driver' => 'file']); 43 | } 44 | } catch (SQLiteDatabaseDoesNotExistException $e) { 45 | header('Location: /error?message=' . urlencode('Frontend not built. Ensure node, npm are installed Run "npm install && npm run build"')); 46 | exit; 47 | } catch (QueryException | \PDOException $e) { 48 | if (str_contains($e->getMessage(), 'readonly database') || str_contains($e->getMessage(), 'open database')) { 49 | http_response_code(500); 50 | echo 'Database error: check permissions on database.sqlite'; 51 | exit; 52 | } 53 | } 54 | RateLimiter::for('login', function (Request $request) { 55 | return Limit::perMinute(7) 56 | ->by($request->ip()) 57 | ->response(function () { 58 | throw ThrottleException::toomany(); 59 | }); 60 | }); 61 | 62 | RateLimiter::for('shared', function (Request $request) { 63 | return Limit::perMinute(20) 64 | ->by($request->ip()) 65 | ->response(function (Request $request, array $headers) { 66 | throw ThrottleException::toomany(); 67 | }); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/Providers/DatabaseFileServiceProvider.php: -------------------------------------------------------------------------------- 1 | uuidService = $uuidService; 16 | } 17 | 18 | public function updateStoragePath(string $storagePath): array 19 | { 20 | try { 21 | $paths = $this->preparePaths($storagePath); 22 | 23 | if (file_exists($storagePath) && ! is_writable($storagePath)) { 24 | return ['status' => false, 'message' => 'Storage directory exists but is not writable']; 25 | } 26 | 27 | if (! $this->ensureDirectoryExists($paths['storageFiles'])) { 28 | return ['status' => false, 'message' => 'Unable to create or write to storage directory. Check Permissions']; 29 | } 30 | 31 | if (! $this->ensureDirectoryExists($paths['thumbnails'])) { 32 | return ['status' => false, 'message' => 'Unable to create or write to thumbnail directory. Check Permissions']; 33 | } 34 | 35 | if (! Setting::updateSetting('storage_path', $storagePath)) { 36 | return ['status' => false, 'message' => 'Failed to save storage path setting']; 37 | } 38 | 39 | return ['status' => true, 'message' => 'Storage path updated successfully']; 40 | } catch (Exception $e) { 41 | return ['status' => false, 'message' => 'An unexpected error occurred: '.$e->getMessage()]; 42 | } 43 | } 44 | 45 | private function preparePaths(string $storagePath): array 46 | { 47 | return [ 48 | 'storageFiles' => $storagePath.DIRECTORY_SEPARATOR.$this->uuidService->getStorageFilesUUID(), 49 | 'thumbnails' => $storagePath.DIRECTORY_SEPARATOR.$this->uuidService->getThumbnailsUUID(), 50 | ]; 51 | } 52 | 53 | private function ensureDirectoryExists(string $path): bool 54 | { 55 | if (file_exists($path)) { 56 | return is_writable($path); 57 | } 58 | 59 | return UploadFileHelper::makeFolder($path) && is_writable($path); 60 | } 61 | 62 | public function getPhpUploadMaxFilesize(): string 63 | { 64 | return (string) ini_get('upload_max_filesize'); 65 | } 66 | 67 | public function getPhpPostMaxSize(): string 68 | { 69 | return (string) ini_get('post_max_size'); 70 | } 71 | 72 | public function getPhpMaxFileUploads(): string 73 | { 74 | return (string) ini_get('max_file_uploads'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/Services/DownloadService.php: -------------------------------------------------------------------------------- 1 | downloadHelper = $downloadHelper; 17 | } 18 | 19 | /** 20 | * @throws FetchFileException 21 | */ 22 | public function generateDownloadPath(Collection $localFiles): string 23 | { 24 | if ($this->isSingleFile($localFiles)) { 25 | return $localFiles[0]->getPrivatePathNameForFile(); 26 | } 27 | 28 | return $this->createZipFile($localFiles); 29 | } 30 | 31 | public function isSingleFile(Collection $localFiles): bool 32 | { 33 | return count($localFiles) === 1 && ! $localFiles[0]->is_dir; 34 | } 35 | 36 | /** 37 | * @throws FetchFileException 38 | */ 39 | public function createZipFile(Collection $localFiles): string 40 | { 41 | $outputZipPath = '/tmp'.DIRECTORY_SEPARATOR.'personal_drive_'.Str::random(4).'_'.now()->format('Y_m_d').'.zip'; 42 | $this->downloadHelper->createZipArchive($localFiles, $outputZipPath); 43 | return $outputZipPath; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Services/FileDeleteService.php: -------------------------------------------------------------------------------- 1 | get() as $file) { 16 | $privateFilePathName = $file->getPrivatePathNameForFile(); 17 | if (!file_exists($privateFilePathName)) { 18 | continue; 19 | } 20 | 21 | if ($this->handleDirectoryDeletion($file, $privateFilePathName, $rootStoragePath)) { 22 | $filesDeleted++; 23 | } 24 | 25 | // Handle file deletion 26 | if ($this->isDeletableFile($file) && unlink($privateFilePathName)) { 27 | $filesDeleted++; 28 | } 29 | } 30 | 31 | return $filesDeleted; 32 | } 33 | 34 | protected function handleDirectoryDeletion(LocalFile $file, string $privateFilePathName, string $rootStoragePath): bool 35 | { 36 | if ($this->isDeletableDirectory($file, $privateFilePathName) && 37 | $this->isDirSubDirOfStorage($privateFilePathName, $rootStoragePath)) { 38 | File::deleteDirectory($privateFilePathName); 39 | $file->deleteUsingPublicPath(); 40 | return true; 41 | } 42 | return false; 43 | } 44 | 45 | public function isDeletableDirectory(LocalFile $file, string $privateFilePathName): bool 46 | { 47 | return $file->is_dir === 1 && file_exists($privateFilePathName) && is_dir($privateFilePathName); 48 | } 49 | 50 | public function isDirSubDirOfStorage(string $privateFilePathName, string $rootStoragePath): string|false 51 | { 52 | return strstr($privateFilePathName, $rootStoragePath); 53 | } 54 | 55 | public function isDeletableFile(LocalFile $file): bool 56 | { 57 | return $file->is_dir === 0; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Services/FileRenameService.php: -------------------------------------------------------------------------------- 1 | pathService = $pathService; 20 | $this->fileOperationsHelper = $fileOperationsHelper; 21 | 22 | } 23 | public function renameFile(LocalFile $file, string $newFilename): void 24 | { 25 | $itemPathName = $file->getPublicPathname(); 26 | $itemPublicDestPathName = $file->public_path . DIRECTORY_SEPARATOR . $newFilename; 27 | $this->fileOperationsHelper->move($itemPathName, $itemPublicDestPathName); 28 | $itemPrivateDestPathName = $this->pathService->getStorageDirPath() . DIRECTORY_SEPARATOR . $itemPublicDestPathName; 29 | 30 | if (!file_exists($itemPrivateDestPathName)) { 31 | throw FileRenameException::couldNotRename(); 32 | } 33 | 34 | 35 | if ($file->is_dir) { 36 | $this->updateDirChildrenRecursively($file, $newFilename); 37 | } 38 | 39 | $updated = $file->update(['filename' => $newFilename]); 40 | 41 | if (!$updated) { 42 | throw FileRenameException::couldNotUpdateIndex(); 43 | } 44 | } 45 | 46 | public function updateDirChildrenRecursively(LocalFile $file, string $newFilename): void 47 | { 48 | $dirPublicPathname = ltrim($file->getPublicPathname(), '/'); 49 | $newFolderPublicPath = ltrim($file->public_path. DIRECTORY_SEPARATOR . $newFilename, '/'); 50 | LocalFile::getByPublicPathLikeSearch($dirPublicPathname) 51 | ->chunk( 52 | 100, 53 | function ($childFiles) use ($dirPublicPathname, $newFolderPublicPath) { 54 | $updates = []; 55 | foreach ($childFiles as $childFile) { 56 | $newPublicPath = $newFolderPublicPath . substr($childFile->public_path, strlen($dirPublicPathname)); 57 | $newPrivatePath = $this->pathService->genPrivatePathFromPublic($newPublicPath); 58 | $updates [] = [ 59 | 'id' => $childFile->id, 60 | 'public_path' => $newPublicPath, 61 | 'private_path' => $newPrivatePath 62 | ]; 63 | } 64 | 65 | foreach ($updates as $update) { 66 | DB::table('local_files') 67 | ->where('id', $update['id']) 68 | ->update(['public_path' => $update['public_path'], 'private_path' => $update['private_path']]); 69 | } 70 | } 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/Services/LPathService.php: -------------------------------------------------------------------------------- 1 | uuidService = $uuidService; 14 | } 15 | 16 | public function cleanDrivePublicPath(string $path): string 17 | { 18 | return preg_replace('#^/drive(/|$)#', '', $path); 19 | } 20 | 21 | public function getStorageDirPath(): string 22 | { 23 | $storagePath = Setting::getSettingByKeyName(Setting::$storagePath); 24 | $uuid = $this->uuidService->getStorageFilesUUID(); 25 | if (! $storagePath || ! $uuid) { 26 | return ''; 27 | } 28 | 29 | return $storagePath.DIRECTORY_SEPARATOR.$uuid; 30 | } 31 | public function getTempStorageDirPath(): string 32 | { 33 | $storagePath = Setting::getSettingByKeyName(Setting::$storagePath); 34 | if (! $storagePath) { 35 | return ''; 36 | } 37 | 38 | return $storagePath.DIRECTORY_SEPARATOR. "temp_storage"; 39 | } 40 | 41 | public function getThumbnailDirPath(): string 42 | { 43 | $storagePath = Setting::getSettingByKeyName(Setting::$storagePath); 44 | $uuid = $this->uuidService->getThumbnailsUUID(); 45 | if (! $storagePath || ! $uuid) { 46 | return ''; 47 | } 48 | 49 | return $storagePath.DIRECTORY_SEPARATOR.$uuid; 50 | } 51 | 52 | public function genPrivatePathFromPublic(string $publicPath = ''): string 53 | { 54 | $privateRoot = $this->getStorageDirPath(); 55 | 56 | if (! $privateRoot) { 57 | return ''; 58 | } 59 | 60 | if ($publicPath === '') { 61 | return $privateRoot.DIRECTORY_SEPARATOR; 62 | } 63 | $publicPath = $this->cleanDrivePublicPath($publicPath); 64 | $privatePath = $privateRoot.DIRECTORY_SEPARATOR.$publicPath.DIRECTORY_SEPARATOR; 65 | 66 | return $privatePath; 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Services/UUIDService.php: -------------------------------------------------------------------------------- 1 | storageFilesUUID = $setting->getSettingByKeyName('uuidForStorageFiles') ?: ''; 20 | $this->thumbnailsUUID = $setting->getSettingByKeyName('uuidForThumbnails') ?: ''; 21 | 22 | if (! $this->storageFilesUUID || ! $this->thumbnailsUUID) { 23 | throw UUIDException::nouuid(); 24 | } 25 | } 26 | 27 | public function getStorageFilesUUID(): string 28 | { 29 | return $this->storageFilesUUID; 30 | } 31 | 32 | public function getThumbnailsUUID(): string 33 | { 34 | return $this->thumbnailsUUID; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Traits/FlashMessages.php: -------------------------------------------------------------------------------- 1 | flash('message', $message); 12 | session()->flash('status'); 13 | if ($moreInfo) { 14 | session()->flash('more_info', $moreInfo); 15 | } 16 | 17 | return redirect()->back(); 18 | } 19 | public function warn(string $message): RedirectResponse 20 | { 21 | session()->flash('message', $message); 22 | session()->flash('status'); 23 | return redirect()->back(); 24 | } 25 | 26 | public function error(string $message): RedirectResponse 27 | { 28 | session()->flash('message', $message); 29 | session()->flash('status', false); 30 | 31 | return redirect()->back(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 18 | web: __DIR__.'/../routes/web.php', 19 | commands: __DIR__.'/../routes/console.php', 20 | health: '/up', 21 | ) 22 | ->withMiddleware(function (Middleware $middleware) { 23 | $middleware->redirectGuestsTo('login'); 24 | $middleware->web(append: [ 25 | HandleInertiaMiddlware::class, 26 | AddLinkHeadersForPreloadedAssets::class, 27 | ], prepend: [ 28 | CheckSetup::class, 29 | ]); 30 | }) 31 | ->withExceptions(function (Exceptions $exceptions) { 32 | $exceptions->render(function (Throwable $e) { 33 | if ($e instanceof FetchFileException) { 34 | return redirect()->route('rejected', ['message' => $e->getMessage()]); 35 | } 36 | if ($e instanceof ThrottleException) { 37 | return redirect()->route('rejected', ['message' => $e->getMessage()]); 38 | } 39 | if ($e instanceof ThumbnailException) { 40 | session()->flash('message', $e->getMessage()); 41 | session()->flash('status', false); 42 | } 43 | if ($e instanceof PersonalDriveException) { 44 | session()->flash('message', $e->getMessage()); 45 | session()->flash('status', false); 46 | 47 | return redirect()->back(); 48 | } 49 | if ($e instanceof ValidationException) { 50 | session()->flash('message', 'Please check the form for errors.'); 51 | session()->flash('status', false); 52 | 53 | return redirect()->back()->withErrors($e->errors()); 54 | } 55 | if ($e instanceof Exception && ! $e instanceof AuthenticationException) { 56 | session()->flash('message', 'Something went wrong!'.$e->getMessage()); 57 | session()->flash('status', false); 58 | 59 | } 60 | if (str_contains($e->getMessage(), 'readonly database') || str_contains($e->getMessage(), 'open database')) { 61 | return redirect()->route('rejected', 'database is readonly ! Make sure database/db/database.sqlite file has write permissions'); 62 | } 63 | }); 64 | })->create(); 65 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Below you may configure as many filesystem disks as necessary, and you 24 | | may even configure multiple disks for the same driver. Examples for 25 | | most supported storage drivers are configured here for reference. 26 | | 27 | | Supported drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app/private'), 36 | 'serve' => true, 37 | 'throw' => false, 38 | ], 39 | 40 | 'public' => [ 41 | 'driver' => 'local', 42 | 'root' => storage_path('app/public'), 43 | 'url' => env('APP_URL').'/storage', 44 | 'visibility' => 'public', 45 | 'throw' => false, 46 | ], 47 | 48 | 's3' => [ 49 | 'driver' => 's3', 50 | 'key' => env('AWS_ACCESS_KEY_ID'), 51 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 52 | 'region' => env('AWS_DEFAULT_REGION'), 53 | 'bucket' => env('AWS_BUCKET'), 54 | 'url' => env('AWS_URL'), 55 | 'endpoint' => env('AWS_ENDPOINT'), 56 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 57 | 'throw' => false, 58 | ], 59 | 60 | ], 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Symbolic Links 65 | |-------------------------------------------------------------------------- 66 | | 67 | | Here you may configure the symbolic links that will be created when the 68 | | `storage:link` Artisan command is executed. The array keys should be 69 | | the locations of the links and the values should be their targets. 70 | | 71 | */ 72 | 73 | 'links' => [ 74 | public_path('storage') => storage_path('app/public'), 75 | ], 76 | 77 | ]; 78 | -------------------------------------------------------------------------------- /config/hashidable.php: -------------------------------------------------------------------------------- 1 | hash('sha256', 'pakistan ki maa ki chut'), 9 | 10 | /** 11 | * Length of the generated hashid. 12 | */ 13 | 'length' => 10, 14 | 15 | /** 16 | * Character set used to generate the hashids. 17 | */ 18 | 'charset' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', 19 | 20 | /** 21 | * Prefix attached to the generated hash. 22 | */ 23 | 'prefix' => '', 24 | 25 | /** 26 | * Suffix attached to the generated hash. 27 | */ 28 | 'suffix' => '', 29 | 30 | /** 31 | * If a prefix of suffix is defined, we use this as a separator 32 | * between the prefix/suffix. 33 | */ 34 | 'separator' => '-', 35 | ]; 36 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'token' => env('POSTMARK_TOKEN'), 19 | ], 20 | 21 | 'ses' => [ 22 | 'key' => env('AWS_ACCESS_KEY_ID'), 23 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 24 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 25 | ], 26 | 27 | 'resend' => [ 28 | 'key' => env('RESEND_KEY'), 29 | ], 30 | 31 | 'slack' => [ 32 | 'notifications' => [ 33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 35 | ], 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /database/factories/FileFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class FileFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => $this->faker->word().'.'.$this->faker->fileExtension(), 21 | 'path' => $this->faker->filePath(), 22 | 'bucket' => $this->faker->word().'_bucket', 23 | 'size' => $this->faker->numberBetween(1024, 1048576), // size in bytes (1KB to 1MB) 24 | 'mime_type' => $this->faker->mimeType(), 25 | 'created_at' => now(), 26 | 'updated_at' => now(), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/factories/SettingFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->word, 16 | 'value' => $this->faker->uuid, 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UserFactory extends Factory 13 | { 14 | /** 15 | * The current password being used by the factory. 16 | */ 17 | protected static ?string $password; 18 | 19 | /** 20 | * Define the model's default state. 21 | * 22 | * @return array 23 | */ 24 | public function definition(): array 25 | { 26 | return [ 27 | 'username' => fake()->name(), 28 | 'password' => static::$password ??= Hash::make('password'), 29 | 'remember_token' => Str::random(10), 30 | ]; 31 | } 32 | 33 | /** 34 | * Indicate that the model's email address should be unverified. 35 | */ 36 | public function unverified(): static 37 | { 38 | return $this->state(fn (array $attributes) => [ 39 | 'email_verified_at' => null, 40 | ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('username')->unique(); 16 | $table->tinyInteger('is_admin')->default(0); 17 | $table->string('password'); 18 | $table->rememberToken(); 19 | $table->timestamps(); 20 | }); 21 | 22 | Schema::create('sessions', function (Blueprint $table) { 23 | $table->string('id')->primary(); 24 | $table->foreignId('user_id')->nullable()->index(); 25 | $table->string('ip_address', 45)->nullable(); 26 | $table->text('user_agent')->nullable(); 27 | $table->longText('payload'); 28 | $table->integer('last_activity')->index(); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | */ 35 | public function down(): void 36 | { 37 | Schema::dropIfExists('users'); 38 | Schema::dropIfExists('password_reset_tokens'); 39 | Schema::dropIfExists('sessions'); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 15 | $table->mediumText('value'); 16 | $table->integer('expiration'); 17 | }); 18 | 19 | Schema::create('cache_locks', function (Blueprint $table) { 20 | $table->string('key')->primary(); 21 | $table->string('owner'); 22 | $table->integer('expiration'); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('cache'); 32 | Schema::dropIfExists('cache_locks'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('queue')->index(); 16 | $table->longText('payload'); 17 | $table->unsignedTinyInteger('attempts'); 18 | $table->unsignedInteger('reserved_at')->nullable(); 19 | $table->unsignedInteger('available_at'); 20 | $table->unsignedInteger('created_at'); 21 | }); 22 | 23 | Schema::create('job_batches', function (Blueprint $table) { 24 | $table->string('id')->primary(); 25 | $table->string('name'); 26 | $table->integer('total_jobs'); 27 | $table->integer('pending_jobs'); 28 | $table->integer('failed_jobs'); 29 | $table->longText('failed_job_ids'); 30 | $table->mediumText('options')->nullable(); 31 | $table->integer('cancelled_at')->nullable(); 32 | $table->integer('created_at'); 33 | $table->integer('finished_at')->nullable(); 34 | }); 35 | 36 | Schema::create('failed_jobs', function (Blueprint $table) { 37 | $table->id(); 38 | $table->string('uuid')->unique(); 39 | $table->text('connection'); 40 | $table->text('queue'); 41 | $table->longText('payload'); 42 | $table->longText('exception'); 43 | $table->timestamp('failed_at')->useCurrent(); 44 | }); 45 | } 46 | 47 | /** 48 | * Reverse the migrations. 49 | */ 50 | public function down(): void 51 | { 52 | Schema::dropIfExists('jobs'); 53 | Schema::dropIfExists('job_batches'); 54 | Schema::dropIfExists('failed_jobs'); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /database/migrations/2024_12_28_130626_create_settings_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('key')->unique(); 16 | $table->text('value'); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::dropIfExists('settings'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2024_12_28_134837_add_default_settings.php: -------------------------------------------------------------------------------- 1 | insert([ 13 | [ 14 | 'key' => 'storage_path', 'value' => '', 'created_at' => now(), 15 | 'updated_at' => now(), 16 | ], 17 | [ 18 | 'key' => 'uuidForStorageFiles', 19 | 'value' => 'storage_personaldrive', 'created_at' => now(), 20 | 'updated_at' => now(), 21 | ], 22 | [ 23 | 'key' => 'uuidForThumbnails', 'value' => 'thumbnail_personaldrive', 'created_at' => now(), 24 | 'updated_at' => now(), 25 | ], 26 | ]); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2024_12_28_181917_create_local_files.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('filename'); 16 | $table->boolean('is_dir'); 17 | $table->string('public_path'); 18 | $table->string('private_path'); 19 | $table->bigInteger('size'); 20 | $table->foreignId('user_id')->constrained()->onDelete('cascade'); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('local_files'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2025_01_02_091305_add_file_type_to_localfiles.php: -------------------------------------------------------------------------------- 1 | string('file_type')->nullable(); 15 | }); 16 | } 17 | 18 | /** 19 | * Reverse the migrations. 20 | */ 21 | public function down(): void 22 | { 23 | Schema::table('localfiles', function (Blueprint $table) { 24 | // 25 | }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/2025_01_03_134103_add_thumbnails.php: -------------------------------------------------------------------------------- 1 | boolean('has_thumbnail')->default(false)->nullable(); 15 | }); 16 | } 17 | 18 | /** 19 | * Reverse the migrations. 20 | */ 21 | public function down(): void 22 | { 23 | // 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /database/migrations/2025_01_04_083848_add_index_to_local_files.php: -------------------------------------------------------------------------------- 1 | unique(['filename', 'public_path']); 15 | }); 16 | } 17 | 18 | /** 19 | * Reverse the migrations. 20 | */ 21 | public function down(): void 22 | { 23 | Schema::table('local_files', function (Blueprint $table) { 24 | // 25 | }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/2025_01_06_132053_add_shares_table.php: -------------------------------------------------------------------------------- 1 | id(); 12 | $table->string('slug')->unique(); 13 | $table->string('password')->nullable(); 14 | $table->timestamp('expiry')->nullable(); 15 | $table->boolean('enabled')->default(true); 16 | $table->timestamps(); 17 | $table->timestamp('last_accessed')->nullable(); 18 | }); 19 | 20 | Schema::create('shared_files', function (Blueprint $table) { 21 | $table->foreignId('share_id')->constrained('shares')->onDelete('cascade'); 22 | $table->foreignId('file_id')->constrained('local_files')->onDelete('cascade'); 23 | $table->primary(['share_id', 'file_id']); 24 | }); 25 | } 26 | 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('shared_files'); 30 | Schema::dropIfExists('shares'); 31 | Schema::dropIfExists('local_files_filename_public_path_unique'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2025_01_07_085657_add_shares_table_accessed_count.php: -------------------------------------------------------------------------------- 1 | integer('accessed_number')->default(0); 15 | }); 16 | } 17 | 18 | /** 19 | * Reverse the migrations. 20 | */ 21 | public function down(): void 22 | { 23 | // 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /database/migrations/2025_01_08_130204_change_id_to_ulid_localfile.php: -------------------------------------------------------------------------------- 1 | ulid('id')->change(); 15 | }); 16 | } 17 | 18 | /** 19 | * Reverse the migrations. 20 | */ 21 | public function down(): void 22 | { 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations/2025_01_08_195508_add_public_path_to_shares_table.php: -------------------------------------------------------------------------------- 1 | string('public_path')->nullable(); 15 | }); 16 | } 17 | 18 | /** 19 | * Reverse the migrations. 20 | */ 21 | public function down(): void 22 | { 23 | Schema::table('shares', function (Blueprint $table) { 24 | // 25 | }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /docker/apache.conf: -------------------------------------------------------------------------------- 1 | 2 | DocumentRoot /var/www/html/personal-drive/public 3 | 4 | 5 | Options Indexes FollowSymLinks 6 | AllowOverride All 7 | Require all granted 8 | 9 | 10 | ErrorLog ${APACHE_LOG_DIR}/error.log 11 | CustomLog ${APACHE_LOG_DIR}/access.log combined 12 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Check if APP_KEY needs to be generated 5 | if [ -z "$(grep '^APP_KEY=..*$' .env)" ]; then 6 | echo "Generating new APP_KEY..." 7 | php artisan key:generate 8 | fi 9 | 10 | # Ensure permissions are set correctly 11 | chown -R www-data:www-data storage bootstrap/cache database /var/www/html/personal-drive-storage-folder 12 | 13 | # Cache configuration 14 | php artisan config:cache 15 | 16 | # Run the main command 17 | exec "$@" -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import react from 'eslint-plugin-react'; 3 | import globals from 'globals'; 4 | import prettier from 'eslint-plugin-prettier'; 5 | import prettierConfig from 'eslint-config-prettier'; 6 | 7 | export default [ 8 | js.configs.recommended, 9 | { 10 | files: ['**/*.js', '**/*.jsx'], 11 | languageOptions: { 12 | parserOptions: { 13 | ecmaVersion: 'latest', 14 | sourceType: 'module', 15 | ecmaFeatures: { 16 | jsx: true, 17 | }, 18 | }, 19 | globals: { 20 | ...globals.browser, // Includes all browser-related globals 21 | route: 'readonly', // Added manually to resolve 'route' is not defined 22 | axios: 'readonly', // Added manually to resolve 'axios' is not defined 23 | }, 24 | }, 25 | plugins: {react}, 26 | rules: { 27 | 'react/react-in-jsx-scope': 'off', 28 | 'react/jsx-uses-vars': 'error', 29 | }, 30 | settings: { 31 | react: { 32 | version: 'detect', 33 | }, 34 | }, 35 | }, 36 | { 37 | files: ['resources/js/**/*.js', 'resources/js/**/*.jsx'], 38 | plugins: { prettier }, 39 | rules: { 40 | 'prettier/prettier': 'error', 41 | }, 42 | }, 43 | ]; -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["resources/js/*"], 6 | "ziggy-js": ["./vendor/tightenco/ziggy"] 7 | } 8 | }, 9 | "exclude": ["node_modules", "public"] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "vite build", 6 | "dev": "vite" 7 | }, 8 | "devDependencies": { 9 | "@eslint/js": "^9.25.0", 10 | "@headlessui/react": "^2.0.0", 11 | "@inertiajs/react": "^1.0.0", 12 | "@tailwindcss/forms": "^0.5.3", 13 | "@vitejs/plugin-react": "^4.2.0", 14 | "autoprefixer": "^10.4.12", 15 | "axios": "^1.7.4", 16 | "concurrently": "^9.0.1", 17 | "eslint": "^9.25.0", 18 | "eslint-config-prettier": "^10.1.2", 19 | "eslint-plugin-prettier": "^5.2.6", 20 | "eslint-plugin-react": "^7.37.5", 21 | "globals": "^16.0.0", 22 | "laravel-vite-plugin": "^1.2", 23 | "postcss": "^8.4.31", 24 | "react": "^18.3.1", 25 | "react-dom": "^18.2.0", 26 | "tailwindcss": "^3.2.1", 27 | "vite": "^6.3" 28 | }, 29 | "dependencies": { 30 | "@tailwindcss/typography": "^0.5.16", 31 | "highlight.js": "^11.11.1", 32 | "js-cookie": "^3.0.5", 33 | "lucide-react": "^0.462.0", 34 | "marked": "^15.0.8", 35 | "marked-highlight": "^2.2.1", 36 | "react-dropzone": "^14.3.8", 37 | "react-icons": "^5.5.0", 38 | "react-onclickoutside": "^6.13.1", 39 | "react-pdf": "^9.2.1", 40 | "react-router-dom": "^7.5.2", 41 | "zustand": "^5.0.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | 4 | # Redirect all requests to index.php 5 | RewriteCond %{REQUEST_FILENAME} !-f 6 | RewriteCond %{REQUEST_FILENAME} !-d 7 | RewriteRule ^ index.php [L] 8 | 9 | SetEnv XDEBUG_SESSION_START 1 10 | 11 | # Prevent directory listing 12 | Options -Indexes 13 | 14 | # Protect .htaccess file 15 | 16 | Order allow,deny 17 | Deny from all 18 | 19 | 20 | # Prevent access to sensitive files -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyaaniguy/personal-drive/1dd2a0ae29c25d00f7b2504742bcebcfdf652c26/public/favicon.ico -------------------------------------------------------------------------------- /public/img/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyaaniguy/personal-drive/1dd2a0ae29c25d00f7b2504742bcebcfdf652c26/public/img/img.png -------------------------------------------------------------------------------- /public/img/list_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyaaniguy/personal-drive/1dd2a0ae29c25d00f7b2504742bcebcfdf652c26/public/img/list_view.png -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyaaniguy/personal-drive/1dd2a0ae29c25d00f7b2504742bcebcfdf652c26/public/img/logo.png -------------------------------------------------------------------------------- /public/img/share-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyaaniguy/personal-drive/1dd2a0ae29c25d00f7b2504742bcebcfdf652c26/public/img/share-screen.png -------------------------------------------------------------------------------- /public/img/sort.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /public/img/tile_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gyaaniguy/personal-drive/1dd2a0ae29c25d00f7b2504742bcebcfdf652c26/public/img/tile_view.png -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 25 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .bg-success { 6 | background-color: #22c55e; 7 | color: #636363; 8 | } 9 | 10 | .bg-error { 11 | background-color: #ef4444; 12 | color: #1c1c1c; 13 | } 14 | 15 | .bg-warning { 16 | background-color: #f59e0b; 17 | color: #636363; 18 | } 19 | 20 | .bg-info { 21 | background-color: #3b82f6; 22 | color: #636363; 23 | } 24 | -------------------------------------------------------------------------------- /resources/js/Components/ApplicationLogo.jsx: -------------------------------------------------------------------------------- 1 | export default function ApplicationLogo() { 2 | return ( 3 | 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /resources/js/Components/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | export default function Checkbox({className = '', ...props}) { 2 | return ( 3 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /resources/js/Components/DangerButton.jsx: -------------------------------------------------------------------------------- 1 | export default function DangerButton({ 2 | className = '', 3 | disabled, 4 | children, 5 | ...props 6 | }) { 7 | return ( 8 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /resources/js/Components/InputError.jsx: -------------------------------------------------------------------------------- 1 | export default function InputError({message, className = '', ...props}) { 2 | return message ? ( 3 |

7 | {message} 8 |

9 | ) : null; 10 | } 11 | -------------------------------------------------------------------------------- /resources/js/Components/InputLabel.jsx: -------------------------------------------------------------------------------- 1 | export default function InputLabel({ 2 | value, 3 | className = '', 4 | children, 5 | ...props 6 | }) { 7 | return ( 8 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /resources/js/Components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import {Dialog, DialogPanel, Transition, TransitionChild,} from '@headlessui/react'; 2 | 3 | export default function Modal({ 4 | children, 5 | show = false, 6 | maxWidth = '2xl', 7 | closeable = true, 8 | onClose = () => { 9 | }, 10 | }) { 11 | const close = () => { 12 | if (closeable) { 13 | onClose(); 14 | } 15 | }; 16 | 17 | const maxWidthClass = { 18 | sm: 'sm:max-w-sm', 19 | md: 'sm:max-w-md', 20 | lg: 'sm:max-w-lg', 21 | xl: 'sm:max-w-xl', 22 | '2xl': 'sm:max-w-2xl', 23 | }[maxWidth]; 24 | 25 | return ( 26 | 27 | 33 | 41 |
42 | 43 | 44 | 52 | 55 | {children} 56 | 57 | 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /resources/js/Components/NavLink.jsx: -------------------------------------------------------------------------------- 1 | import {Link} from '@inertiajs/react'; 2 | 3 | export default function NavLink({ 4 | active = false, 5 | className = '', 6 | children, 7 | ...props 8 | }) { 9 | return ( 10 | 20 | {children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /resources/js/Components/PrimaryButton.jsx: -------------------------------------------------------------------------------- 1 | export default function PrimaryButton({ 2 | className = '', 3 | disabled, 4 | children, 5 | ...props 6 | }) { 7 | return ( 8 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /resources/js/Components/ResponsiveNavLink.jsx: -------------------------------------------------------------------------------- 1 | import {Link} from '@inertiajs/react'; 2 | 3 | export default function ResponsiveNavLink({ 4 | active = false, 5 | className = '', 6 | children, 7 | ...props 8 | }) { 9 | return ( 10 | 18 | {children} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /resources/js/Components/SecondaryButton.jsx: -------------------------------------------------------------------------------- 1 | export default function SecondaryButton({ 2 | type = 'button', 3 | className = '', 4 | disabled, 5 | children, 6 | ...props 7 | }) { 8 | return ( 9 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /resources/js/Components/TextInput.jsx: -------------------------------------------------------------------------------- 1 | import {forwardRef, useEffect, useImperativeHandle, useRef} from 'react'; 2 | 3 | export default forwardRef(function TextInput( 4 | {type = 'text', className = '', isFocused = false, ...props}, 5 | ref, 6 | ) { 7 | const localRef = useRef(null); 8 | 9 | useImperativeHandle(ref, () => ({ 10 | focus: () => localRef.current?.focus(), 11 | })); 12 | 13 | useEffect(() => { 14 | if (isFocused) { 15 | localRef.current?.focus(); 16 | } 17 | }, [isFocused]); 18 | 19 | return ( 20 | 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /resources/js/Contexts/CutFilesContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState } from "react"; 2 | 3 | // Create the context 4 | export const CutFilesContext = createContext(); 5 | 6 | // Create a provider component 7 | export function CutFilesProvider({ children }) { 8 | const [cutFiles, setCutFiles] = useState(new Set()); 9 | const [cutPath, setCutPath] = useState(""); 10 | return ( 11 | 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /resources/js/Pages/Admin/Layouts/SetupRaw.jsx: -------------------------------------------------------------------------------- 1 | import AlertBox from "@/Pages/Drive/Components/AlertBox.jsx"; 2 | 3 | export default function SetupRaw({ children }) { 4 | return ( 5 | <> 6 |
7 |

8 | PersonalDrive Setup 9 |

10 |
11 | 12 |
13 | {children} 14 |
15 |
16 |
17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /resources/js/Pages/Admin/Setup.jsx: -------------------------------------------------------------------------------- 1 | import { router } from "@inertiajs/react"; 2 | import { useState } from "react"; 3 | import SetupRaw from "./Layouts/SetupRaw.jsx"; 4 | 5 | export default function Setup() { 6 | const [formData, setFormData] = useState({ 7 | username: "", 8 | password: "", 9 | }); 10 | 11 | function handleChange(e) { 12 | setFormData((oldValues) => ({ 13 | ...oldValues, 14 | [e.target.id]: e.target.value, 15 | })); 16 | } 17 | 18 | function handleSubmit(e) { 19 | e.preventDefault(); 20 | router.post("/setup/account", formData); 21 | } 22 | 23 | return ( 24 | 25 |

26 | Create the admin account 27 |

28 |
29 |
30 | 36 | 46 |
47 |
48 | 54 | 63 |
64 | 70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/Breadcrumb.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@inertiajs/react"; 2 | import { ChevronRight, HomeIcon } from "lucide-react"; 3 | 4 | export default function Breadcrumb({ path, isAdmin }) { 5 | let rootLink = isAdmin ? "/drive" : "/shared"; 6 | let links = []; 7 | if (path) { 8 | let pathArr = path.split("/"); 9 | pathArr.shift(); 10 | pathArr.shift(); 11 | for (let link of pathArr) { 12 | rootLink += "/" + link; 13 | links.push({ name: link, href: rootLink }); 14 | } 15 | } 16 | 17 | return ( 18 | <> 19 | {links.length > 0 && ( 20 | 71 | )} 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/CreateFolderModal.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Modal from "./Modal.jsx"; 3 | import { router } from "@inertiajs/react"; 4 | 5 | const CreateItemModal = ({ isModalOpen, setIsModalOpen, path, isFile }) => { 6 | const [itemName, setItemName] = useState(""); 7 | 8 | const handleSubmit = async (e) => { 9 | e.preventDefault(); 10 | const formData = {}; 11 | formData["path"] = path; 12 | formData["itemName"] = itemName; 13 | formData["isFile"] = isFile.current; 14 | setIsModalOpen(false); 15 | router.post("/create-item", formData, { 16 | only: ["files", "flash"], 17 | }); 18 | setItemName(""); 19 | }; 20 | 21 | return ( 22 | 28 |
29 |
33 |
34 | 42 | setItemName(e.target.value)} 47 | className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50 bg-gray-800" 48 | required 49 | /> 50 |
51 | 57 |
58 |
59 |
60 | ); 61 | }; 62 | 63 | export default CreateItemModal; 64 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/CutButton.jsx: -------------------------------------------------------------------------------- 1 | import { ScissorsIcon } from "lucide-react"; 2 | import Button from "./Generic/Button.jsx"; 3 | 4 | const CutButton = ({ classes, onCut }) => { 5 | return ( 6 | 13 | ); 14 | }; 15 | 16 | export default CutButton; 17 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/DeleteButton.jsx: -------------------------------------------------------------------------------- 1 | import { Trash2Icon } from "lucide-react"; 2 | import { router } from "@inertiajs/react"; 3 | import Button from "./Generic/Button.jsx"; 4 | 5 | const DeleteButton = ({ 6 | setSelectedFiles, 7 | selectedFiles, 8 | classes, 9 | setSelectAllToggle, 10 | }) => { 11 | const confirmAndDelete = (e) => { 12 | if (window.confirm("Confirm Deletion?")) { 13 | deleteFilesComponentHandler(e); 14 | } 15 | }; 16 | 17 | async function deleteFilesComponentHandler(e) { 18 | e.stopPropagation(); 19 | router.post( 20 | "/delete-files", 21 | { 22 | fileList: Array.from(selectedFiles), 23 | }, 24 | { 25 | preserveState: true, 26 | preserveScroll: true, 27 | only: ["files", "flash"], 28 | onFinish: () => { 29 | setSelectedFiles?.(new Set()); 30 | setSelectAllToggle?.(false); 31 | }, 32 | }, 33 | ); 34 | } 35 | 36 | return ( 37 | 46 | ); 47 | }; 48 | 49 | export default DeleteButton; 50 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/DropZone.jsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { useDropzone } from "react-dropzone"; 3 | 4 | function FileDropzone({ onFilesAccepted }) { 5 | const [isDragActive, setIsDragActive] = useState(false); 6 | 7 | useEffect(() => { 8 | const handleDragEnter = (e) => { 9 | if (e.dataTransfer?.types?.includes("Files")) { 10 | setIsDragActive(true); 11 | } 12 | }; 13 | 14 | const handleDragLeave = (e) => { 15 | if (e.relatedTarget === null) { 16 | setIsDragActive(false); 17 | } 18 | }; 19 | 20 | window.addEventListener("dragenter", handleDragEnter); 21 | window.addEventListener("dragleave", handleDragLeave); 22 | window.addEventListener("drop", () => setIsDragActive(false)); 23 | 24 | return () => { 25 | window.removeEventListener("dragenter", handleDragEnter); 26 | window.removeEventListener("dragleave", handleDragLeave); 27 | window.removeEventListener("drop", () => setIsDragActive(false)); 28 | }; 29 | }, []); 30 | 31 | const onDrop = useCallback( 32 | (acceptedFiles) => { 33 | setIsDragActive(false); 34 | onFilesAccepted(acceptedFiles); 35 | }, 36 | [onFilesAccepted], 37 | ); 38 | 39 | const { getRootProps, getInputProps, isDragAccept } = useDropzone({ 40 | onDrop, 41 | noClick: true, 42 | noKeyboard: true, 43 | }); 44 | 45 | if (!isDragActive) return null; 46 | 47 | return ( 48 |
setIsDragActive(false)} // Click anywhere to dismiss 51 | style={{ 52 | position: "fixed", 53 | top: 0, 54 | left: 0, 55 | width: "100vw", 56 | height: "100vh", 57 | backgroundColor: "rgba(0, 0, 0, 0.5)", 58 | zIndex: 9999, 59 | display: "flex", 60 | justifyContent: "center", 61 | alignItems: "center", 62 | border: `4px dashed ${isDragAccept ? "#00e676" : "#ffffff"}`, 63 | borderRadius: "8px", 64 | color: "white", 65 | fontSize: "24px", 66 | cursor: "pointer", 67 | }} 68 | > 69 | 70 | {isDragAccept 71 | ? "Drop files here to upload" 72 | : "Drag files here to upload"} 73 |
74 | ); 75 | } 76 | 77 | export default FileDropzone; 78 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/FileItem.jsx: -------------------------------------------------------------------------------- 1 | import { File } from "lucide-react"; 2 | import DownloadButton from "./DownloadButton.jsx"; 3 | import DeleteButton from "@/Pages/Drive/Components/DeleteButton.jsx"; 4 | import React from "react"; 5 | import ShowShareModalButton from "@/Pages/Drive/Components/Shares/ShowShareModalButton.jsx"; 6 | import RenameModalButton from "@/Pages/Drive/Components/Shares/RenameModalButton.jsx"; 7 | 8 | const FileItem = React.memo(function FileItem({ 9 | file, 10 | isSearch, 11 | token, 12 | setStatusMessage, 13 | setAlertStatus, 14 | handleFileClick, 15 | setIsShareModalOpen, 16 | setFilesToShare, 17 | isAdmin, 18 | slug, 19 | setSelectedFiles, 20 | setIsRenameModalOpen, 21 | setFileToRename, 22 | }) { 23 | return ( 24 |
handleFileClick(file)} 27 | > 28 |
29 | 30 | 31 | {(isSearch ? file.public_path + "/" : "") + file.filename} 32 | 33 |
34 |
35 | {isAdmin && ( 36 | 41 | )} 42 | 50 | {isAdmin && ( 51 | <> 52 | 58 | 64 | 65 | )} 66 |
67 |
68 | ); 69 | }); 70 | 71 | export default FileItem; 72 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/FileList/FileListRow.jsx: -------------------------------------------------------------------------------- 1 | import FileItem from "../FileItem.jsx"; 2 | import FolderItem from "../FolderItem.jsx"; 3 | import React from "react"; 4 | 5 | const FileListRow = React.memo(function FileListRow({ 6 | file, 7 | isSearch, 8 | token, 9 | setStatusMessage, 10 | setAlertStatus, 11 | handleFileClick, 12 | isSelected, 13 | handlerSelectFile, 14 | setIsShareModalOpen, 15 | setFilesToShare, 16 | isAdmin, 17 | path, 18 | slug, 19 | setSelectedFiles, 20 | setIsRenameModalOpen, 21 | setFileToRename, 22 | }) { 23 | return ( 24 |
25 |
handlerSelectFile(file)} 28 | > 29 | {}} 33 | /> 34 |
35 |
36 | {file.is_dir ? ( 37 | 52 | ) : ( 53 | 69 | )} 70 |
71 |
{file.sizeText}
72 |
{file.file_type}
73 |
74 | ); 75 | }); 76 | 77 | export default FileListRow; 78 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/FileList/ImageViewer.jsx: -------------------------------------------------------------------------------- 1 | const ImageViewer = ({ id, slug }) => { 2 | let src = "/fetch-file/" + id; 3 | src += slug ? "/" + slug : ""; 4 | return ( 5 | Selected File 10 | ); 11 | }; 12 | 13 | export default ImageViewer; 14 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/FileList/PdfViewer.jsx: -------------------------------------------------------------------------------- 1 | import { Document, Page, pdfjs } from "react-pdf"; 2 | import { useState } from "react"; 3 | import "react-pdf/dist/esm/Page/AnnotationLayer.css"; 4 | import "react-pdf/dist/esm/Page/TextLayer.css"; 5 | import { ArrowLeft, ArrowRight } from "lucide-react"; 6 | import Button from "../Generic/Button.jsx"; 7 | 8 | pdfjs.GlobalWorkerOptions.workerSrc = new URL( 9 | "pdfjs-dist/build/pdf.worker.min.mjs", 10 | import.meta.url, 11 | ).toString(); 12 | 13 | const PdfViewer = ({ id, slug }) => { 14 | const [numPages, setNumPages] = useState(0); 15 | const [pageNumber, setPageNumber] = useState(1); 16 | let src = "/fetch-file/" + id; 17 | src += slug ? "/" + slug : ""; 18 | 19 | function onDocumentLoadSuccess({ numPages }) { 20 | setNumPages(numPages); 21 | } 22 | 23 | return ( 24 |
25 |

26 | 27 | Page {pageNumber} of {numPages} 28 | 29 |

30 | 38 | 46 |
47 |

48 | 53 | console.error("Error loading PDF:", error) 54 | } 55 | > 56 | 57 | 58 |
59 | ); 60 | }; 61 | 62 | export default PdfViewer; 63 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/FileList/RenameModal.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import Modal from "../Modal.jsx"; 3 | import { router } from "@inertiajs/react"; 4 | 5 | const RenameModal = ({ 6 | isRenameModalOpen, 7 | setIsRenameModalOpen, 8 | setFileToRename, 9 | fileToRename, 10 | }) => { 11 | const [formData, setFormData] = useState(() => ({ 12 | id: fileToRename?.id || "", 13 | filename: fileToRename?.filename || "", 14 | })); 15 | 16 | useEffect(() => { 17 | if (fileToRename) { 18 | setFormData({ 19 | id: fileToRename.id, 20 | filename: fileToRename.filename, 21 | }); 22 | } 23 | }, [fileToRename]); 24 | const handleChange = (e) => { 25 | const { id, value } = e.target; 26 | setFormData((prevState) => ({ 27 | ...prevState, 28 | [id]: value, 29 | })); 30 | }; 31 | 32 | function handleCloseRenameModal(status) { 33 | setIsRenameModalOpen(status); 34 | setFileToRename?.(new Set()); 35 | } 36 | 37 | const handleSubmit = async (e) => { 38 | e.preventDefault(); 39 | router.post( 40 | "/rename-file", 41 | { 42 | ...formData, 43 | }, 44 | { 45 | preserveState: true, 46 | preserveScroll: true, 47 | only: ["files", "flash", "errors"], 48 | onSuccess: () => { 49 | handleCloseRenameModal(false); 50 | }, 51 | onFinish: () => {}, 52 | }, 53 | ); 54 | }; 55 | 56 | return ( 57 | 63 |
64 |
68 |
69 | 77 |
78 | 84 |
85 |
86 |
87 | ); 88 | }; 89 | 90 | export default RenameModal; 91 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/FileList/VideoPlayer.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | const VideoPlayer = ({ id, slug }) => { 4 | let src = "/fetch-file/" + id; 5 | 6 | src += slug ? "/" + slug : ""; 7 | const [autoplay, setAutoplay] = useState(() => { 8 | const savedAutoplay = localStorage.getItem("videoAutoplay"); 9 | return savedAutoplay !== null ? JSON.parse(savedAutoplay) : false; 10 | }); 11 | const videoRef = useRef(null); 12 | 13 | useEffect(() => { 14 | if (videoRef.current) { 15 | videoRef.current.autoplay = autoplay; 16 | } 17 | }, [autoplay]); 18 | 19 | const handleAutoplayToggle = () => { 20 | localStorage.setItem("videoAutoplay", JSON.stringify(!autoplay)); 21 | setAutoplay(!autoplay); 22 | }; 23 | 24 | return ( 25 |
26 | 36 |
37 | 44 | 50 |
51 |
52 | ); 53 | }; 54 | 55 | export default VideoPlayer; 56 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/FolderItem.jsx: -------------------------------------------------------------------------------- 1 | import { Folder } from "lucide-react"; 2 | import { Link } from "@inertiajs/react"; 3 | import DownloadButton from "./DownloadButton.jsx"; 4 | import DeleteButton from "@/Pages/Drive/Components/DeleteButton.jsx"; 5 | import React from "react"; 6 | import ShowShareModalButton from "@/Pages/Drive/Components/Shares/ShowShareModalButton.jsx"; 7 | import RenameModalButton from "@/Pages/Drive/Components/Shares/RenameModalButton.jsx"; 8 | 9 | const FolderItem = React.memo(function FolderItem({ 10 | file, 11 | isSelected, 12 | isSearch, 13 | token, 14 | setStatusMessage, 15 | setAlertStatus, 16 | setIsShareModalOpen, 17 | setFilesToShare, 18 | isAdmin, 19 | path, 20 | slug, 21 | setSelectedFiles, 22 | setIsRenameModalOpen, 23 | setFileToRename, 24 | }) { 25 | return ( 26 |
29 | 39 |
40 | 41 | 42 | {(isSearch ? file.public_path + "/" : "") + 43 | file.filename} 44 | 45 |
46 | 47 | 48 |
49 | {isAdmin && ( 50 | 55 | )} 56 | 64 | {isAdmin && ( 65 | <> 66 | 72 | 78 | 79 | )} 80 |
81 |
82 | ); 83 | }); 84 | export default FolderItem; 85 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/Generic/Button.jsx: -------------------------------------------------------------------------------- 1 | const Button = ({ onClick, classes, children, ...props }) => { 2 | return ( 3 | 10 | ); 11 | }; 12 | 13 | export default Button; 14 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/Modal.jsx: -------------------------------------------------------------------------------- 1 | const Modal = ({ 2 | isOpen, 3 | onClose, 4 | title, 5 | children, 6 | classes, 7 | shouldCloseOnOverlayClick = true, 8 | }) => { 9 | return ( 10 | isOpen && ( 11 |
shouldCloseOnOverlayClick && onClose(false)} 14 | > 15 |
e.stopPropagation()} 18 | > 19 | {title && ( 20 |
21 |

{title}

22 | 40 |
41 | )} 42 |
43 | {children} 44 |
45 |
46 |
47 | ) 48 | ); 49 | }; 50 | 51 | export default Modal; 52 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/PasteButton.jsx: -------------------------------------------------------------------------------- 1 | import { ClipboardCopyIcon } from "lucide-react"; 2 | import Button from "./Generic/Button.jsx"; 3 | 4 | const PasteButton = ({ classes, onPaste }) => { 5 | return ( 6 | 15 | ); 16 | }; 17 | 18 | export default PasteButton; 19 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/RefreshButton.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { router } from "@inertiajs/react"; 5 | import Button from "./Generic/Button.jsx"; 6 | import { RefreshCwIcon } from "lucide-react"; 7 | 8 | export default function RefreshButton() { 9 | const [isLoading, setIsLoading] = useState(false); 10 | 11 | async function handleClick(e) { 12 | e.preventDefault(); 13 | setIsLoading(true); 14 | router.post( 15 | "/resync", 16 | {}, 17 | { 18 | preserveState: true, 19 | preserveScroll: true, 20 | only: ["files", "flash"], 21 | onFinish: () => { 22 | setIsLoading(false); 23 | }, 24 | }, 25 | ); 26 | } 27 | 28 | return ( 29 |
30 | 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function SearchBar({ handleSearch }) { 4 | const [searchValue, setSearchValue] = useState(""); 5 | const clearSearch = () => { 6 | setSearchValue(""); 7 | }; 8 | return ( 9 |
10 |
11 |
12 | setSearchValue(e.target.value)} 19 | /> 20 | {searchValue && ( 21 | 28 | )} 29 |
30 | 31 | 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/Shares/RenameModalButton.jsx: -------------------------------------------------------------------------------- 1 | import { TextCursorIcon } from "lucide-react"; 2 | import Button from "@/Pages/Drive/Components/Generic/Button.jsx"; 3 | 4 | const RenameModalButton = ({ 5 | classes = "", 6 | setIsRenameModalOpen, 7 | setFileToRename, 8 | fileToRename, 9 | }) => { 10 | function handleShareButton(e) { 11 | e.stopPropagation(); 12 | setIsRenameModalOpen(true); 13 | setFileToRename(fileToRename); 14 | } 15 | 16 | return ( 17 | 24 | ); 25 | }; 26 | 27 | export default RenameModalButton; 28 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Components/Shares/ShowShareModalButton.jsx: -------------------------------------------------------------------------------- 1 | import { Share2Icon } from "lucide-react"; 2 | import Button from "@/Pages/Drive/Components/Generic/Button.jsx"; 3 | 4 | const ShowShareModalButton = ({ 5 | setIsShareModalOpen, 6 | classes = "", 7 | setFilesToShare, 8 | filesToShare, 9 | }) => { 10 | function handleShareButton(e) { 11 | e.stopPropagation(); 12 | setIsShareModalOpen(true); 13 | if (setFilesToShare) { 14 | setFilesToShare(filesToShare); 15 | } 16 | } 17 | 18 | return ( 19 | 28 | ); 29 | }; 30 | 31 | export default ShowShareModalButton; 32 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/DriveHome.jsx: -------------------------------------------------------------------------------- 1 | import Header from "@/Pages/Drive/Layouts/Header.jsx"; 2 | import FileBrowserSection from "@/Pages/Drive/Components/FileBrowserSection.jsx"; 3 | 4 | export default function DriveHome({ files, path, token }) { 5 | return ( 6 | <> 7 |
8 |
9 |
10 | 16 |
17 |
18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Hooks/useClickOutside.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | function useClickOutside(ref, handler) { 4 | useEffect(() => { 5 | const listener = (event) => { 6 | // Do nothing if clicking ref's element or descendent elements 7 | if (!ref.current || ref.current.contains(event.target)) { 8 | return; 9 | } 10 | handler(event); 11 | }; 12 | 13 | document.addEventListener("mousedown", listener); 14 | document.addEventListener("touchstart", listener); 15 | 16 | return () => { 17 | document.removeEventListener("mousedown", listener); 18 | document.removeEventListener("touchstart", listener); 19 | }; 20 | }, [ref, handler]); 21 | } 22 | 23 | export default useClickOutside; 24 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Hooks/useSearchUtil.jsx: -------------------------------------------------------------------------------- 1 | import { router } from "@inertiajs/react"; 2 | 3 | function useSearchUtil() { 4 | async function handleSearch(e, searchText) { 5 | e.preventDefault(); 6 | router.post( 7 | "/search-files", 8 | { query: searchText }, 9 | { 10 | onSuccess: () => {}, 11 | }, 12 | ); 13 | } 14 | 15 | return { handleSearch }; 16 | } 17 | 18 | export default useSearchUtil; 19 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Hooks/useSelectionutil.jsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | 3 | function useSelectionUtil() { 4 | const [selectedFiles, setSelectedFiles] = useState(new Set()); 5 | const [selectAllToggle, setSelectAllToggle] = useState(false); 6 | 7 | function handlerSelectFile(file) { 8 | setSelectedFiles((prevSelectedFiles) => { 9 | const newSelectedFiles = new Set(prevSelectedFiles); 10 | newSelectedFiles.has(file.id) 11 | ? newSelectedFiles.delete(file.id) // Toggle off 12 | : newSelectedFiles.add(file.id); // Toggle on 13 | return newSelectedFiles; 14 | }); 15 | } 16 | 17 | const handlerSelectFileMemo = useCallback(handlerSelectFile, []); 18 | 19 | function handleSelectAllToggle(files) { 20 | // if false -> select all files | else -> deselect all files 21 | if (selectAllToggle) { 22 | setSelectedFiles(new Set()); 23 | setSelectAllToggle(false); 24 | } else { 25 | setSelectedFiles(new Set(files.map((file) => file.id))); 26 | setSelectAllToggle(true); 27 | } 28 | } 29 | 30 | return { 31 | selectAllToggle, 32 | handleSelectAllToggle, 33 | selectedFiles, 34 | setSelectedFiles, 35 | setSelectAllToggle, 36 | handlerSelectFileMemo, 37 | }; 38 | } 39 | 40 | export default useSelectionUtil; 41 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Hooks/useThumbnailGenerator.jsx: -------------------------------------------------------------------------------- 1 | import { router } from "@inertiajs/react"; 2 | 3 | const useThumbnailGenerator = (files, path) => { 4 | const generateThumbnails = async (ids) => { 5 | await router.post("/gen-thumbs", { ids, path }); 6 | }; 7 | 8 | // Filter files that need thumbnails 9 | const thumbnailIds = files 10 | .filter( 11 | (file) => 12 | !file.has_thumbnail && 13 | ["image", "video"].includes(file.file_type), 14 | ) 15 | .map((file) => file.id); 16 | if (thumbnailIds.length > 0) { 17 | generateThumbnails(thumbnailIds, path); 18 | } 19 | }; 20 | 21 | export default useThumbnailGenerator; 22 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Layouts/GuestLayout.jsx: -------------------------------------------------------------------------------- 1 | import ApplicationLogo from "@/Components/ApplicationLogo.jsx"; 2 | import { Link } from "@inertiajs/react"; 3 | 4 | export default function GuestLayout({ children }) { 5 | return ( 6 |
7 |
8 | 9 | 10 | 11 |
12 | 13 |
14 | {children} 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/ShareFilesGuestHome.jsx: -------------------------------------------------------------------------------- 1 | import useSelectionUtil from "@/Pages/Drive/Hooks/useSelectionutil.jsx"; 2 | import FileBrowserSection from "@/Pages/Drive/Components/FileBrowserSection.jsx"; 3 | import AlertBox from "@/Pages/Drive/Components/AlertBox.jsx"; 4 | import { useState } from "react"; 5 | 6 | export default function ShareFilesGuestHome({ files, path, token, slug }) { 7 | const { 8 | selectAllToggle, 9 | handleSelectAllToggle, 10 | selectedFiles, 11 | handlerSelectFileMemo, 12 | } = useSelectionUtil(); 13 | const [statusMessage, setStatusMessage] = useState(""); 14 | 15 | return ( 16 |
17 |
18 |
19 | 20 |
21 |
22 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Shares/CheckSharePassword.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { router } from "@inertiajs/react"; 3 | import AlertBox from "@/Pages/Drive/Components/AlertBox.jsx"; 4 | 5 | const CheckSharePassword = ({ slug }) => { 6 | const [password, setPassword] = useState(""); 7 | const [error, setError] = useState(null); 8 | 9 | const handleSubmit = (e) => { 10 | e.preventDefault(); 11 | 12 | router.post( 13 | "/shared-check-password", 14 | { slug: slug, password: password }, 15 | { 16 | onError: (errors) => setError(errors.password || null), 17 | }, 18 | ); 19 | }; 20 | 21 | return ( 22 |
23 |
24 | 25 |
26 |

Enter Password For Share

27 |
28 | setPassword(e.target.value)} 33 | placeholder="Password" 34 | required 35 | /> 36 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default CheckSharePassword; 49 | -------------------------------------------------------------------------------- /resources/js/Pages/Drive/Svgs/SortIcon.jsx: -------------------------------------------------------------------------------- 1 | const SortIcon = ({ classes = "" }) => { 2 | return ( 3 | 13 | ); 14 | }; 15 | 16 | export default SortIcon; 17 | -------------------------------------------------------------------------------- /resources/js/Pages/Rejected.jsx: -------------------------------------------------------------------------------- 1 | export default function Rejected({ message }) { 2 | message = message || "You do not have permission to view this page"; 3 | return ( 4 |
5 |
6 |

7 | Access Denied 8 |

9 |

{message}

10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /resources/js/app.jsx: -------------------------------------------------------------------------------- 1 | import '../css/app.css'; 2 | import './bootstrap'; 3 | 4 | import { createInertiaApp } from '@inertiajs/react'; 5 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; 6 | import { createRoot } from 'react-dom/client'; 7 | import { BrowserRouter } from 'react-router-dom' 8 | import { CutFilesProvider } from './Contexts/CutFilesContext'; 9 | 10 | 11 | const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; 12 | 13 | createInertiaApp({ 14 | title: (title) => `${title} - ${appName}`, 15 | resolve: (name) => 16 | resolvePageComponent( 17 | `./Pages/${name}.jsx`, 18 | import.meta.glob('./Pages/**/*.jsx'), 19 | ), 20 | setup({ el, App, props }) { 21 | const root = createRoot(el); 22 | 23 | root.render( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }, 33 | progress: { 34 | color: '#22BFFA', 35 | showSpinner: true, 36 | 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | window.axios = axios; 4 | 5 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 6 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ config('app.name', 'Laravel') }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | @routes 15 | @viteReactRefresh 16 | @vite(['resources/js/app.jsx', "resources/js/Pages/{$page['component']}.jsx"]) 17 | @inertiaHead 18 | 19 | 20 | @inertia 21 | 22 | 23 | -------------------------------------------------------------------------------- /routes/auth.php: -------------------------------------------------------------------------------- 1 | group(function () { 8 | Route::get('login', [AuthenticatedSessionController::class, 'create']) 9 | ->name('login'); 10 | 11 | Route::post('login', [AuthenticatedSessionController::class, 'store'])->middleware(['throttle:login']); 12 | }); 13 | 14 | Route::middleware('auth')->group(function () { 15 | Route::put('password', [PasswordController::class, 'update'])->name('password.update'); 16 | 17 | Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) 18 | ->name('logout'); 19 | }); 20 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 8 | })->purpose('Display an inspiring quote')->hourly(); 9 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | # Check dependencies 6 | REQUIRED_TOOLS=(npm node php composer) 7 | for tool in "${REQUIRED_TOOLS[@]}"; do 8 | if ! command -v $tool &>/dev/null; then 9 | echo "Error: $tool is not installed. Please install it first." 10 | exit 1 11 | fi 12 | done 13 | 14 | 15 | # Default values 16 | WEB_USER="www-data" 17 | WEB_GROUP="www-data" 18 | 19 | # Function to ask for user input 20 | ask_for_value() { 21 | local prompt="$1" 22 | local default_value="$2" 23 | local user_value 24 | 25 | read -p "$prompt [$default_value]: " user_value 26 | echo "${user_value:-$default_value}" 27 | } 28 | 29 | # echo "You can change these values if needed." 30 | # Ask the user to confirm or change the web server user and group 31 | WEB_USER=$(ask_for_value "Enter the web server user" "$WEB_USER") 32 | WEB_GROUP=$(ask_for_value "Enter the web server group" "$WEB_GROUP") 33 | 34 | 35 | echo "Setting up environment file..." 36 | cp .env.example .env 37 | 38 | # Ask for APP_URL 39 | APP_URL=$(ask_for_value "Enter the application URL (leave empty to skip)" "") 40 | if [ -n "$APP_URL" ]; then 41 | sed -i "s|^APP_URL=.*|APP_URL=$APP_URL|" .env 42 | fi 43 | 44 | # Check if the directory exists before creating it 45 | if [ ! -d "database/db" ]; then 46 | mkdir database/db 47 | fi 48 | echo "Installing composer dependencies..." 49 | composer install --no-interaction --prefer-dist 50 | 51 | echo "Installing npm dependencies..." 52 | npm install && npm run build 53 | 54 | 55 | echo "Generating application key..." 56 | php artisan key:generate --force 57 | 58 | # Set permissions 59 | echo "Attempting to change ownership to $WEB_USER:$WEB_GROUP..." 60 | if sudo chown -R $WEB_USER:$WEB_GROUP storage bootstrap/cache database 2>/dev/null; then 61 | echo "Ownership changed successfully." 62 | else 63 | echo "Could not change owner. Insufficient permissions. Please fix manually." 64 | fi 65 | 66 | echo "Setting directory permissions..." 67 | if sudo chmod -R 775 storage bootstrap/cache database 2>/dev/null; then 68 | echo "Permissions updated successfully." 69 | else 70 | echo "Could not change permissions. Please update manually." 71 | fi 72 | 73 | 74 | echo "Clearing and caching config..." 75 | php artisan config:clear 76 | php artisan config:cache 77 | 78 | echo "Setup complete!" 79 | 80 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !private/ 3 | !public/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/debugbar/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'tailwindcss/defaultTheme'; 2 | import forms from '@tailwindcss/forms'; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | content: [ 7 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 8 | './storage/framework/views/*.php', 9 | './resources/views/**/*.blade.php', 10 | './resources/js/**/*.jsx', 11 | './resources/**/*.jsx', 12 | ], 13 | 14 | theme: { 15 | extend: { 16 | fontFamily: { 17 | sans: ['Figtree', ...defaultTheme.fontFamily.sans], 18 | }, 19 | }, 20 | }, 21 | 22 | plugins: [ 23 | forms, 24 | require('@tailwindcss/typography'), 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /tests/Feature/Auth/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 6 | 7 | $response->assertStatus(302); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/Feature/Helpers/FileSizeFormatterTest.php: -------------------------------------------------------------------------------- 1 | toBe('1 KB') 7 | ->and(FileSizeFormatter::format(100))->toBe('1 KB'); 8 | }); 9 | 10 | it('formats bytes in KB', function () { 11 | expect(FileSizeFormatter::format(1024))->toBe('1 KB') 12 | ->and(FileSizeFormatter::format(2048))->toBe('2 KB') 13 | ->and(FileSizeFormatter::format(5148))->toBe('5 KB'); 14 | }); 15 | 16 | it('formats bytes in MB', function () { 17 | expect(FileSizeFormatter::format(1048576))->toBe('1 MB') 18 | ->and(FileSizeFormatter::format(2048000))->toBe('1.95 MB') 19 | ->and(FileSizeFormatter::format(9048000))->toBe('8.63 MB'); 20 | }); 21 | 22 | it('formats bytes in GB', function () { 23 | expect(FileSizeFormatter::format(1073741824))->toBe('1 GB') 24 | ->and(FileSizeFormatter::format(2000000000))->toBe('1.86 GB'); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/Feature/Helpers/ResponseHelperTest.php: -------------------------------------------------------------------------------- 1 | toBeInstanceOf(JsonResponse::class) 10 | ->and($response->getData(true)) 11 | ->toMatchArray([ 12 | 'status' => true, 13 | 'message' => 'Success message', 14 | ]); 15 | }); 16 | 17 | it('returns a failed JSON response', function () { 18 | $response = ResponseHelper::json('Error message', false); 19 | 20 | expect($response)->toBeInstanceOf(JsonResponse::class) 21 | ->and($response->getData(true)) 22 | ->toMatchArray([ 23 | 'status' => false, 24 | 'message' => 'Error message', 25 | ]); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/Feature/Helpers/UploadFileHelperTest.php: -------------------------------------------------------------------------------- 1 | toBe('/path/to/file.txt'); 13 | }); 14 | 15 | it('returns the realtive path', function () { 16 | $_FILES['files']['full_path'][0] = './file.txt'; 17 | $fullPath = UploadFileHelper::getUploadedFileFullPath(0); 18 | expect($fullPath)->toBe('/file.txt'); 19 | }); 20 | 21 | it('returns the realtive path 2', function () { 22 | $_FILES['files']['full_path'][0] = '/file.txt'; 23 | $fullPath = UploadFileHelper::getUploadedFileFullPath(0); 24 | expect($fullPath)->toBe('/file.txt'); 25 | }); 26 | 27 | it('creates a folder with the specified permissions', function () { 28 | $path = __DIR__ . '/test_folder'; 29 | $result = UploadFileHelper::makeFolder($path, 0750); 30 | expect($result)->toBeTrue() 31 | ->and(is_dir($path))->toBeTrue() 32 | ->and(decoct(fileperms($path) & 0777))->toBe('750'); 33 | rmdir($path); // Clean up 34 | }); 35 | 36 | it('throws an exception if the folder already exists', function () { 37 | $path = __DIR__; // Existing folder 38 | expect(function () use ($path) { 39 | UploadFileHelper::makeFolder($path); 40 | })->toThrow(Exception::class, "Could not create new folder"); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/Feature/Services/UUIDServiceTest.php: -------------------------------------------------------------------------------- 1 | settingMock = mock(Setting::class); 8 | 9 | // Set up the mock expectations 10 | $this->settingMock->shouldReceive('getSettingByKeyName') 11 | ->with('uuidForStorageFiles') 12 | ->andReturn('storage-123'); 13 | 14 | $this->settingMock->shouldReceive('getSettingByKeyName') 15 | ->with('uuidForThumbnails') 16 | ->andReturn('thumb-456'); 17 | 18 | $this->app->instance(Setting::class, $this->settingMock); 19 | }); 20 | 21 | 22 | it('successfully initializes with valid UUIDs', function () { 23 | $service = new UUIDService($this->settingMock); 24 | 25 | expect($service->getStorageFilesUUID())->toBe('storage-123') 26 | ->and($service->getThumbnailsUUID())->toBe('thumb-456'); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/Feature/Traits/FlashMessagesTest.php: -------------------------------------------------------------------------------- 1 | flashMessages = new class () { 10 | use FlashMessages; 11 | }; 12 | }); 13 | 14 | it('sets success message and redirects back', function () { 15 | $response = $this->flashMessages->success('Operation successful'); 16 | 17 | expect(session()->get('message'))->toBe('Operation successful') 18 | ->and(session()->get('status'))->toBe(true) 19 | ->and($response)->toBeInstanceOf(RedirectResponse::class); 20 | }); 21 | 22 | it('sets error message and redirects back', function () { 23 | $response = $this->flashMessages->error('Operation failed'); 24 | 25 | expect(session()->get('message'))->toBe('Operation failed') 26 | ->and(session()->get('status'))->toBe(false) 27 | ->and($response)->toBeInstanceOf(RedirectResponse::class); 28 | }); 29 | 30 | it('verifies flash messages are actually flashed', function () { 31 | $this->flashMessages->success('Operation failed'); 32 | 33 | expect(session()->has('message'))->toBeTrue() 34 | ->and(session()->has('status'))->toBeTrue(); 35 | 36 | session()->driver()->save(); // Persist the data 37 | // Clear the session data 38 | session()->flush(); 39 | session()->regenerate(); // Regenerate the session ID 40 | session()->start(); // Start a new session 41 | expect(session()->has('message'))->toBeFalse() 42 | ->and(session()->has('status'))->toBeFalse(); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | extend(Tests\TestCase::class) 15 | ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) 16 | ->in('Feature'); 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Expectations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When you're writing tests, you often need to check that values meet certain conditions. The 24 | | "expect()" function gives you access to a set of "expectations" methods that you can use 25 | | to assert different things. Of course, you may extend the Expectation API at any time. 26 | | 27 | */ 28 | 29 | expect()->extend('toBeOne', function () { 30 | return $this->toBe(1); 31 | }); 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Functions 36 | |-------------------------------------------------------------------------- 37 | | 38 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 39 | | project that you don't want to repeat in every file. Here you can also expose helpers as 40 | | global functions to help you to reduce the number of lines of code in your test files. 41 | | 42 | */ 43 | 44 | function something() 45 | { 46 | // .. 47 | } 48 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/Unit/Services/DownloadServiceTest.php: -------------------------------------------------------------------------------- 1 | downloadHelperMock = $this->createMock(DownloadHelper::class); 20 | $this->downloadService = new DownloadService($this->downloadHelperMock); 21 | } 22 | 23 | public function testGenerateDownloadPathSingleFile() 24 | { 25 | $file = $this->createMock(LocalFile::class); 26 | $file->is_dir = false; 27 | $file->method('getPrivatePathNameForFile')->willReturn('/path/to/file'); 28 | 29 | $localFiles = new Collection([$file]); 30 | 31 | $result = $this->downloadService->generateDownloadPath($localFiles); 32 | 33 | $this->assertEquals('/path/to/file', $result); 34 | } 35 | 36 | public function testGenerateDownloadPathSingleDir() 37 | { 38 | $file = $this->createMock(LocalFile::class); 39 | $file->method('getPrivatePathNameForFile')->willReturn('/path/to/aDir'); 40 | $file->method('__get')->with('is_dir')->willReturn(true); 41 | 42 | 43 | $localFiles = new Collection([$file]); 44 | $this->downloadHelperMock->expects($this->once()) 45 | ->method('createZipArchive') 46 | ->with($localFiles, $this->anything()); 47 | $result = $this->downloadService->generateDownloadPath($localFiles); 48 | 49 | $this->assertStringContainsString('/tmp/personal_drive_', $result); 50 | $this->assertStringEndsWith('.zip', $result); 51 | } 52 | 53 | public function testGenerateDownloadPathMultipleFiles() 54 | { 55 | $file1 = $this->createMock(LocalFile::class); 56 | $file1->is_dir = false; 57 | 58 | $file2 = $this->createMock(LocalFile::class); 59 | $file2->is_dir = false; 60 | 61 | $localFiles = new Collection([$file1, $file2]); 62 | 63 | $this->downloadHelperMock->expects($this->once()) 64 | ->method('createZipArchive') 65 | ->with($localFiles, $this->anything()); 66 | 67 | $result = $this->downloadService->generateDownloadPath($localFiles); 68 | 69 | $this->assertStringContainsString('/tmp/personal_drive_', $result); 70 | $this->assertStringEndsWith('.zip', $result); 71 | } 72 | 73 | public function testIsSingleFile() 74 | { 75 | $file = $this->createMock(LocalFile::class); 76 | $file->is_dir = false; 77 | 78 | $localFiles = new Collection([$file]); 79 | 80 | $result = $this->downloadService->isSingleFile($localFiles); 81 | 82 | $this->assertTrue($result); 83 | } 84 | 85 | public function testIsNotSingleFile() 86 | { 87 | $file1 = $this->createMock(LocalFile::class); 88 | $file1->is_dir = false; 89 | 90 | $file2 = $this->createMock(LocalFile::class); 91 | $file2->is_dir = false; 92 | 93 | $localFiles = new Collection([$file1, $file2]); 94 | 95 | $result = $this->downloadService->isSingleFile($localFiles); 96 | 97 | $this->assertFalse($result); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/Unit/Services/FileDeleteServiceTest.php: -------------------------------------------------------------------------------- 1 | fileDeleteService = new FileDeleteService(); 18 | $this->tempDir = sys_get_temp_dir() . '/testDir'; 19 | mkdir($this->tempDir); 20 | } 21 | 22 | 23 | protected function tearDown(): void 24 | { 25 | if (is_dir($this->tempDir)) { 26 | rmdir($this->tempDir); 27 | } 28 | 29 | parent::tearDown(); 30 | } 31 | 32 | public function testIsDeletableDirectory() 33 | { 34 | $file = $this->createMock(LocalFile::class); 35 | $file->method('__get')->with('is_dir')->willReturn(1); 36 | 37 | $result = $this->fileDeleteService->isDeletableDirectory($file, $this->tempDir); 38 | 39 | $this->assertTrue($result); 40 | } 41 | 42 | 43 | 44 | public function testIsDirSubDirOfStorage() 45 | { 46 | $result = $this->fileDeleteService->isDirSubDirOfStorage('/path/to/dir', '/path/to'); 47 | $this->assertNotFalse($result); 48 | } 49 | 50 | public function testIsDeletableFile() 51 | { 52 | $file = $this->createMock(LocalFile::class); 53 | $file->method('__get')->with('is_dir')->willReturn(0); 54 | 55 | $result = $this->fileDeleteService->isDeletableFile($file); 56 | 57 | $this->assertTrue($result); 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /tests/Unit/Services/LPathServiceTest.php: -------------------------------------------------------------------------------- 1 | uuidService = $this->createMock(UUIDService::class); 20 | $this->lPathService = new LPathService($this->uuidService); 21 | $this->lPathServiceMock = $this->getMockBuilder(LPathService::class) 22 | ->setConstructorArgs([$this->uuidService]) 23 | ->onlyMethods(['getStorageDirPath']) 24 | ->getMock(); 25 | } 26 | 27 | public function testCleanDrivePublicPath() 28 | { 29 | $result = $this->lPathService->cleanDrivePublicPath('this/folder/drive'); 30 | self::assertEquals($result, 'this/folder/drive'); 31 | 32 | $result = $this->lPathService->cleanDrivePublicPath('/drive/my/drive'); 33 | self::assertEquals($result, 'my/drive'); 34 | 35 | $result = $this->lPathService->cleanDrivePublicPath('/drive/'); 36 | self::assertEquals($result, ''); 37 | 38 | $result = $this->lPathService->cleanDrivePublicPath('/drive'); 39 | self::assertEquals($result, ''); 40 | 41 | $result = $this->lPathService->cleanDrivePublicPath('/drivemy/drive'); 42 | self::assertEquals($result, '/drivemy/drive'); 43 | } 44 | 45 | public function testGenPrivatePathFromPublicWithEmptyPublicPath() 46 | { 47 | $this->lPathServiceMock->method('getStorageDirPath')->willReturn('test/storage/path/test-uuid'); 48 | 49 | $result = $this->lPathServiceMock->genPrivatePathFromPublic(''); 50 | $this->assertEquals('test/storage/path/test-uuid/', $result); 51 | } 52 | 53 | public function testGenPrivatePathFromPublicWithNonExistentPath() 54 | { 55 | 56 | $this->lPathServiceMock->method('getStorageDirPath')->willReturn('/test/storage/path/test-uuid'); 57 | 58 | $result = $this->lPathServiceMock->genPrivatePathFromPublic('/drive/nonexistent/path'); 59 | $this->assertEquals('/test/storage/path/test-uuid/nonexistent/path/', $result); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | export default defineConfig({ 6 | base: process.env.VITE_ASSET_URL || process.env.ASSET_URL || './', 7 | server: false, 8 | plugins: [ 9 | laravel({ 10 | input: 'resources/js/app.jsx', 11 | refresh: true, 12 | }), 13 | react({ 14 | strictMode: false 15 | }), 16 | ], 17 | }); 18 | --------------------------------------------------------------------------------