From ba3741d45769105fa2bece092b89070ce47c5864 Mon Sep 17 00:00:00 2001 From: Micilini Roll Date: Sat, 9 May 2026 21:31:34 -0300 Subject: [PATCH] fix attachment runtime directory for composer installs --- .gitignore | 1 + README.md | 17 ++++++ examples/easy-chat/server.php | 3 +- examples/medium-chat/server.php | 3 +- examples/private-chat/server.php | 3 +- src/Chat/ChatKernel.php | 3 +- src/Storage/File/FileAttachmentStore.php | 14 ++++- src/Support/RuntimePath.php | 52 +++++++++++++++++++ .../Examples/ExampleServerAutoloadTest.php | 25 +++++++++ .../Unit/Storage/FileAttachmentStoreTest.php | 19 +++++++ tests/Unit/Support/RuntimePathTest.php | 38 ++++++++++++++ 11 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 src/Support/RuntimePath.php create mode 100644 tests/Unit/Support/RuntimePathTest.php diff --git a/.gitignore b/.gitignore index 3873cbf..e7e3c36 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ desktop.ini /storage/*.sqlite3 /storage/*.db /storage/attachments/ +/.phpsockets/ /examples/**/storage/*.sqlite /examples/**/storage/*.sqlite3 /examples/**/storage/*.db diff --git a/README.md b/README.md index 69120e7..674b445 100644 --- a/README.md +++ b/README.md @@ -461,6 +461,7 @@ PHPSOCKETS_HOST=127.0.0.1 PHPSOCKETS_PORT=8080 PHPSOCKETS_MAX_PAYLOAD_BYTES=4194304 PHPSOCKETS_MAX_ATTACHMENT_BYTES=2097152 +PHPSOCKETS_ATTACHMENT_DIR=.phpsockets/attachments PHPSOCKETS_STORAGE=memory PHPSOCKETS_DEBUG=true ``` @@ -562,6 +563,22 @@ php artisan phpsockets:migrate --driver=sqlite --database=database/phpsockets.sq The examples support small file messages. +### Attachment runtime directory + +By default, PHPSockets stores temporary example attachments in a project-local directory: + +```txt +.phpsockets/attachments +``` + +You can override this location with: + +```env +PHPSOCKETS_ATTACHMENT_DIR=/absolute/path/to/attachments +``` + +This is especially useful for production deployments, Laravel apps, containers and Windows environments. + Supported MIME types: ```txt diff --git a/examples/easy-chat/server.php b/examples/easy-chat/server.php index 098d70b..4ad6de3 100644 --- a/examples/easy-chat/server.php +++ b/examples/easy-chat/server.php @@ -10,9 +10,10 @@ $host = getenv('PHPSOCKETS_HOST') ?: '127.0.0.1'; $port = (int) (getenv('PHPSOCKETS_PORT') ?: 8080); +$publicPath = str_replace('\\', '/', __DIR__ . '/public'); echo "PHPSockets EasyChat server running on ws://{$host}:{$port}\n"; -echo "Open the browser UI with: php -S 127.0.0.1:8000 -t examples/easy-chat/public\n"; +echo "Open the browser UI with: php -S 127.0.0.1:8000 -t {$publicPath}\n"; echo "Press Ctrl+C to stop the WebSocket server.\n\n"; $server = ChatServer::create( diff --git a/examples/medium-chat/server.php b/examples/medium-chat/server.php index f1b7ec1..0a0cc5e 100644 --- a/examples/medium-chat/server.php +++ b/examples/medium-chat/server.php @@ -14,9 +14,10 @@ $host = getenv('PHPSOCKETS_HOST') ?: '127.0.0.1'; $port = (int) (getenv('PHPSOCKETS_PORT') ?: 8080); +$publicPath = str_replace('\\', '/', __DIR__ . '/public'); echo "PHPSockets MediumChat server running on ws://{$host}:{$port}\n"; -echo "Open the browser UI with: php -S 127.0.0.1:8001 -t examples/medium-chat/public\n"; +echo "Open the browser UI with: php -S 127.0.0.1:8001 -t {$publicPath}\n"; echo "Press Ctrl+C to stop the WebSocket server.\n\n"; $server = ChatServer::create( diff --git a/examples/private-chat/server.php b/examples/private-chat/server.php index a167df6..faaccbe 100644 --- a/examples/private-chat/server.php +++ b/examples/private-chat/server.php @@ -17,9 +17,10 @@ $host = getenv('PHPSOCKETS_HOST') ?: '127.0.0.1'; $port = (int) (getenv('PHPSOCKETS_PORT') ?: 8080); +$publicPath = str_replace('\\', '/', __DIR__ . '/public'); echo "PHPSockets PrivateChat server running on ws://{$host}:{$port}\n"; -echo "Open the browser UI with: php -S 127.0.0.1:8002 -t examples/private-chat/public\n"; +echo "Open the browser UI with: php -S 127.0.0.1:8002 -t {$publicPath}\n"; echo "Press Ctrl+C to stop the WebSocket server.\n\n"; $server = ChatServer::create( diff --git a/src/Chat/ChatKernel.php b/src/Chat/ChatKernel.php index d420db6..14ff4d6 100644 --- a/src/Chat/ChatKernel.php +++ b/src/Chat/ChatKernel.php @@ -23,6 +23,7 @@ use Micilini\PhpSockets\Storage\InMemory\InMemoryMessageStore; use Micilini\PhpSockets\Storage\InMemory\InMemoryRoomStore; use Micilini\PhpSockets\Storage\InMemory\InMemorySessionStore; +use Micilini\PhpSockets\Support\RuntimePath; use Throwable; final class ChatKernel @@ -57,7 +58,7 @@ public function __construct( $this->rooms = $roomStore ?? new InMemoryRoomStore(); $this->validator = new PayloadValidator(); $this->attachmentValidator = new AttachmentValidator($this->config); - $this->attachments = $attachmentStore ?? new FileAttachmentStore(sys_get_temp_dir() . '/phpsockets-attachments'); + $this->attachments = $attachmentStore ?? new FileAttachmentStore(RuntimePath::attachmentsDirectory()); $this->bots = $botManager ?? new BotManager(); $this->presence = new PresenceManager( new UsernameNormalizer($this->config->maxDisplayNameLength), diff --git a/src/Storage/File/FileAttachmentStore.php b/src/Storage/File/FileAttachmentStore.php index 7b78dc7..8214e84 100644 --- a/src/Storage/File/FileAttachmentStore.php +++ b/src/Storage/File/FileAttachmentStore.php @@ -116,11 +116,23 @@ private function metadataPath(string $attachmentId): string private function ensureDirectory(string $path): void { if (is_dir($path)) { + if (!is_writable($path)) { + throw new StorageException("Attachment directory is not writable: {$path}"); + } + return; } - if (!mkdir($path, 0775, true) && !is_dir($path)) { + if (file_exists($path)) { + throw new StorageException("Attachment path exists but is not a directory: {$path}"); + } + + if (!@mkdir($path, 0775, true) && !is_dir($path)) { throw new StorageException("Failed to create attachment directory: {$path}"); } + + if (!is_writable($path)) { + throw new StorageException("Attachment directory is not writable: {$path}"); + } } } diff --git a/src/Support/RuntimePath.php b/src/Support/RuntimePath.php new file mode 100644 index 0000000..70e35ec --- /dev/null +++ b/src/Support/RuntimePath.php @@ -0,0 +1,52 @@ +path); self::assertFileExists($this->directory . DIRECTORY_SEPARATOR . $attachment->id . '.json'); } + + public function testConstructorFailsWhenAttachmentPathExistsAsFile(): void + { + $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpsockets-attachment-file-conflict-' . uniqid('', true); + + file_put_contents($path, 'not a directory'); + + try { + $this->expectException(StorageException::class); + $this->expectExceptionMessage('Attachment path exists but is not a directory'); + + new FileAttachmentStore($path); + } finally { + if (is_file($path)) { + unlink($path); + } + } + } } diff --git a/tests/Unit/Support/RuntimePathTest.php b/tests/Unit/Support/RuntimePathTest.php new file mode 100644 index 0000000..9c9b3bb --- /dev/null +++ b/tests/Unit/Support/RuntimePathTest.php @@ -0,0 +1,38 @@ +