├── .gitattributes
├── .gitignore
├── LICENSE.md
├── composer.json
├── composer.lock
├── phpcs.xml
├── phpstan.neon
├── phpunit.xml
├── readme.md
└── src
├── LaravelSmbAdapterProvider.php
└── SmbAdapter.php
/.gitattributes:
--------------------------------------------------------------------------------
1 | /.github export-ignore
2 | /tests export-ignore
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /.phpunit.result.cache
3 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Jerodev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jerodev/flysystem-v3-smb-adapter",
3 | "description": "SMB adapter for Flysystem v3",
4 | "keywords": [
5 | "filesystem",
6 | "flysystem",
7 | "laravel",
8 | "samba",
9 | "smb"
10 | ],
11 | "type": "library",
12 | "license": "MIT",
13 | "authors": [
14 | {
15 | "name": "jerodev",
16 | "email": "jeroen@deviaene.eu"
17 | }
18 | ],
19 | "repositories": [
20 | {
21 | "type": "vcs",
22 | "url": "git@github.com:jerodev/code-styles.git"
23 | }
24 | ],
25 | "autoload": {
26 | "psr-4": {
27 | "Jerodev\\Flysystem\\Smb\\": "src/"
28 | }
29 | },
30 | "autoload-dev": {
31 | "psr-4": {
32 | "Jerodev\\Flysystem\\Smb\\Tests\\": "tests/"
33 | }
34 | },
35 | "require": {
36 | "php": ">=8.0.2",
37 | "icewind/smb": "^3.5",
38 | "league/flysystem": "^3.0"
39 | },
40 | "require-dev": {
41 | "illuminate/filesystem": "^9.0",
42 | "illuminate/support": "^9.0",
43 | "jerodev/code-styles": "dev-master",
44 | "league/flysystem-adapter-test-utilities": "^3.0",
45 | "orchestra/testbench": "^7.7",
46 | "phpstan/phpstan": "^1.4",
47 | "phpunit/phpunit": "^9.5"
48 | },
49 | "suggest": {
50 | "ext-smbclient": "Required to use this package"
51 | },
52 | "extra": {
53 | "laravel": {
54 | "providers": [
55 | "Jerodev\\Flysystem\\Smb\\LaravelSmbAdapterProvider"
56 | ]
57 | }
58 | },
59 | "config": {
60 | "optimize-autoloader": true,
61 | "preferred-install": "dist",
62 | "sort-packages": true,
63 | "allow-plugins": {
64 | "dealerdirect/phpcodesniffer-composer-installer": true
65 | },
66 | "platform": {
67 | "php": "8.0.2"
68 | }
69 | },
70 | "minimum-stability": "dev",
71 | "prefer-stable": true
72 | }
73 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | src
9 |
10 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: max
3 | paths:
4 | - src
5 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 | tests
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # SMB adapter for Flysystem v3
2 | [](https://github.com/jerodev/flysystem-v3-smb-adapter/actions/workflows/run-tests.yml) [](https://packagist.org/packages/jerodev/flysystem-v3-smb-adapter)
3 |
4 | This package enables you to communicate with SMB shares through [Flysystem v3](https://github.com/thephpleague/flysystem).
5 |
6 | ## Installation
7 |
8 | composer require jerodev/flysystem-v3-smb-adapter
9 |
10 | ## Usage
11 |
12 | The adapter uses the [Icewind SMB](https://github.com/icewind1991/SMB) package to communicate with the share.
13 | To use the flysystem adapter, you have to pass it an instance of [`\Icewind\SMB\IShare`](https://github.com/icewind1991/SMB/blob/master/src/IShare.php). Below is an example of how to create a share instance using the factory provided by Icewind SMB.
14 |
15 | ```php
16 | $server = (new \Icewind\SMB\ServerFactory())->createServer(
17 | $config->host,
18 | new \Icewind\SMB\BasicAuth(
19 | $config->user,
20 | 'test',
21 | $config->password
22 | )
23 | );
24 | $share = $server->getShare($config->share);
25 |
26 | return new \Jerodev\Flysystem\Smb\SmbAdapter($share, '');
27 | ```
28 |
29 | ## Laravel Filesystem
30 | The package also ships with a Laravel service provider that automatically registers a driver for you. Laravel will discover this provider for you when installing this package.
31 | All you have to do is configure the share in your `config/filesystems.php` similar to the example below.
32 |
33 | ```php
34 | 'disks' => [
35 | 'smb_share' => [
36 | 'driver' => 'smb',
37 | 'workgroup' => 'WORKGROUP',
38 | 'host' => \env('SMB_HOST', '127.0.0.1'),
39 | 'path' => \env('SMB_PATH', 'test'),
40 | 'username' => \env('SMB_USERNAME', ''),
41 | 'password' => \env('SMB_PASSWORD', ''),
42 |
43 | // Optional Icewind SMB options
44 | 'smb_version_min' => \Icewind\SMB\IOptions::PROTOCOL_SMB2,
45 | 'smb_version_max' => \Icewind\SMB\IOptions::PROTOCOL_SMB2_24,
46 | 'timeout' => 20,
47 | ],
48 | ],
49 | ```
50 |
--------------------------------------------------------------------------------
/src/LaravelSmbAdapterProvider.php:
--------------------------------------------------------------------------------
1 | setMinProtocol($config['smb_version_min'] ?? null);
20 | $options->setMaxProtocol($config['smb_version_max'] ?? null);
21 | $options->setTimeout($config['timeout'] ?? 20);
22 |
23 | $server = (new ServerFactory($options))->createServer(
24 | $config['host'],
25 | new BasicAuth($config['username'], $config['workgroup'] ?? 'WORKGROUP', $config['password'])
26 | );
27 | $share = $server->getShare($config['path']);
28 | $adapter = new SmbAdapter($share);
29 |
30 | return new FilesystemAdapter(
31 | new Filesystem($adapter, $config),
32 | $adapter,
33 | $config,
34 | );
35 | });
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/SmbAdapter.php:
--------------------------------------------------------------------------------
1 | */
29 | private array $fakeVisibility = [];
30 |
31 | public function __construct(
32 | private IShare $share,
33 | string $prefix = '',
34 | ) {
35 | $this->mimeTypeDetector = new FinfoMimeTypeDetector();
36 | $this->prefixer = new PathPrefixer($prefix, DIRECTORY_SEPARATOR);
37 | }
38 |
39 | public function fileExists(string $path): bool
40 | {
41 | $location = $this->prefixer->prefixPath($path);
42 |
43 | try {
44 | $this->share->stat($location);
45 | } catch (NotFoundException) {
46 | return false;
47 | }
48 |
49 | return true;
50 | }
51 |
52 | public function directoryExists(string $path): bool
53 | {
54 | return $this->fileExists($path);
55 | }
56 |
57 | public function write(string $path, string $contents, Config $config): void
58 | {
59 | $this->recursiveCreateDir(\dirname($path));
60 |
61 | $location = $this->prefixer->prefixPath($path);
62 |
63 | try {
64 | $stream = $this->share->write($location);
65 | \fwrite($stream, $contents);
66 |
67 | if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {
68 | $this->fakeVisibility[$path] = \strval($visibility);
69 | }
70 | } catch (Throwable $e) {
71 | throw UnableToWriteFile::atLocation($location, '', $e);
72 | } finally {
73 | if (isset($stream)) {
74 | \fclose($stream);
75 | }
76 | }
77 | }
78 |
79 | public function writeStream(string $path, $resource, Config $config): void
80 | {
81 | $this->recursiveCreateDir(\dirname($path));
82 |
83 | $location = $this->prefixer->prefixPath($path);
84 |
85 | try {
86 | $stream = $this->share->write($location);
87 | \stream_copy_to_stream($resource, $stream);
88 |
89 | if ($visibility = $config->get(Config::OPTION_VISIBILITY)) {
90 | $this->fakeVisibility[$path] = \strval($visibility);
91 | }
92 | } catch (Throwable $e) {
93 | throw UnableToWriteFile::atLocation($location, '', $e);
94 | } finally {
95 | if (isset($stream)) {
96 | \fclose($stream);
97 | }
98 | }
99 | }
100 |
101 | public function read(string $path): string
102 | {
103 | try {
104 | $stream = $this->readStream($path);
105 | $contents = \stream_get_contents($stream);
106 | } catch (Throwable $e) {
107 | throw UnableToReadFile::fromLocation($path, $e->getMessage(), $e);
108 | } finally {
109 | if (isset($stream)) {
110 | \fclose($stream);
111 | }
112 | }
113 |
114 | if ($contents === false) {
115 | throw UnableToReadFile::fromLocation($path);
116 | }
117 |
118 | return $contents;
119 | }
120 |
121 | public function readStream(string $path)
122 | {
123 | $location = $this->prefixer->prefixPath($path);
124 |
125 | try {
126 | $stream = $this->share->read($location);
127 | } catch (Throwable $e) {
128 | throw UnableToReadFile::fromLocation($location, '', $e);
129 | }
130 |
131 | return $stream;
132 | }
133 |
134 | public function delete(string $path): void
135 | {
136 | $location = $this->prefixer->prefixPath($path);
137 |
138 | try {
139 | $this->share->del($location);
140 | } catch (NotFoundException) {
141 | // We should ignore exceptions if the file did not exist in the first place.
142 | } catch (Throwable $e) {
143 | throw UnableToDeleteFile::atLocation($location, $e->getMessage(), $e);
144 | }
145 | }
146 |
147 | public function deleteDirectory(string $path): void
148 | {
149 | $location = $this->prefixer->prefixPath($path);
150 |
151 | try {
152 | $this->share->rmdir($location);
153 | } catch (Throwable $e) {
154 | throw UnableToDeleteDirectory::atLocation($location, $e->getMessage(), $e);
155 | }
156 | }
157 |
158 | public function createDirectory(string $path, Config $config): void
159 | {
160 | try {
161 | $this->recursiveCreateDir($path);
162 | } catch (Throwable $e) {
163 | throw UnableToCreateDirectory::dueToFailure($path, $e);
164 | }
165 | }
166 |
167 | public function setVisibility(string $path, string $visibility): void
168 | {
169 | if (! $this->fileExists($path)) {
170 | throw UnableToSetVisibility::atLocation($path, 'File does not exist');
171 | }
172 |
173 | $this->fakeVisibility[$path] = $visibility;
174 | }
175 |
176 | public function visibility(string $path): FileAttributes
177 | {
178 | return $this->getFileAttributes($path);
179 | }
180 |
181 | public function mimeType(string $path): FileAttributes
182 | {
183 | try {
184 | $resource = $this->readStream($path);
185 | } catch (Throwable $e) {
186 | throw UnableToRetrieveMetadata::mimeType($path, $e->getMessage(), $e);
187 | }
188 |
189 | $mimeType = $this->mimeTypeDetector->detectMimeType($path, $resource);
190 | \fclose($resource);
191 |
192 | if ($mimeType === null) {
193 | throw UnableToRetrieveMetadata::mimeType($path, \error_get_last()['message'] ?? '');
194 | }
195 |
196 | return new FileAttributes($path, null, null, null, $mimeType);
197 | }
198 |
199 | public function lastModified(string $path): FileAttributes
200 | {
201 | return $this->getFileAttributes($path);
202 | }
203 |
204 | public function fileSize(string $path): FileAttributes
205 | {
206 | return $this->getFileAttributes($path);
207 | }
208 |
209 | public function listContents(string $path, bool $deep): iterable
210 | {
211 | foreach ($this->share->dir($path) as $fileInfo) {
212 | if ($fileInfo->isDirectory()) {
213 | yield new DirectoryAttributes($fileInfo->getPath(), $this->fakeVisibility[$fileInfo->getPath()] ?? null, $fileInfo->getMTime());
214 | if ($deep) {
215 | foreach ($this->listContents($fileInfo->getPath(), true) as $deepFileInfo) {
216 | yield $deepFileInfo;
217 | }
218 | }
219 | } else {
220 | yield new FileAttributes($fileInfo->getPath(), $fileInfo->getSize(), $this->fakeVisibility[$fileInfo->getPath()] ?? null, $fileInfo->getMTime());
221 | }
222 | }
223 | }
224 |
225 | public function move(string $source, string $destination, Config $config): void
226 | {
227 | try {
228 | $this->share->rename($source, $destination);
229 | } catch (Throwable $e) {
230 | throw UnableToMoveFile::fromLocationTo($source, $destination, $e);
231 | }
232 | }
233 |
234 | public function copy(string $source, string $destination, Config $config): void
235 | {
236 | $content = $this->read($source);
237 | $this->write($destination, $content, $config);
238 |
239 | $this->fakeVisibility[$destination] = \strval($config->get(Config::OPTION_VISIBILITY, $this->fakeVisibility[$source] ?? null));
240 | }
241 |
242 | /** Recursively remove all data from a folder */
243 | public function clearDir(string $path): void
244 | {
245 | foreach ($this->listContents($path, false) as $content) {
246 | if ($content instanceof DirectoryAttributes) {
247 | $this->clearDir($content->path());
248 | $this->deleteDirectory($content->path());
249 | } else {
250 | $this->delete($content->path());
251 | }
252 | }
253 | }
254 |
255 | protected function recursiveCreateDir(string $path): void
256 | {
257 | if ($path === '.' || $this->directoryExists($path)) {
258 | return;
259 | }
260 |
261 | $directories = \explode(DIRECTORY_SEPARATOR, $path);
262 | if (\count($directories) > 1) {
263 | $parentDirectories = \array_splice($directories, 0, \count($directories) - 1);
264 | $this->recursiveCreateDir(\implode(DIRECTORY_SEPARATOR, $parentDirectories));
265 | }
266 |
267 | $location = $this->prefixer->prefixPath($path);
268 |
269 | $this->share->mkdir($location);
270 | }
271 |
272 | protected function getFileAttributes(string $path): FileAttributes
273 | {
274 | $location = $this->prefixer->prefixPath($path);
275 |
276 | try {
277 | $fileInfo = $this->share->stat($location);
278 | } catch (Throwable $e) {
279 | throw UnableToRetrieveMetadata::lastModified($location, '', $e);
280 | }
281 |
282 | if ($fileInfo->isDirectory()) {
283 | throw UnableToRetrieveMetadata::lastModified($location, "'{$path}' is a directory");
284 | }
285 |
286 | return new FileAttributes(
287 | $location,
288 | $fileInfo->getSize(),
289 | $this->fakeVisibility[$path] ?? null,
290 | $fileInfo->getMTime(),
291 | );
292 | }
293 | }
294 |
--------------------------------------------------------------------------------