1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Copyright (c) 2014 Robin Appelman <[email protected]> |
4
|
|
|
* This file is licensed under the Licensed under the MIT license: |
5
|
|
|
* http://opensource.org/licenses/MIT |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace Icewind\SMB; |
9
|
|
|
|
10
|
|
|
use Icewind\SMB\Exception\ConnectionException; |
11
|
|
|
use Icewind\SMB\Exception\FileInUseException; |
12
|
|
|
use Icewind\SMB\Exception\InvalidTypeException; |
13
|
|
|
use Icewind\SMB\Exception\NotFoundException; |
14
|
|
|
use Icewind\Streams\CallbackWrapper; |
15
|
|
|
|
16
|
|
|
class Share extends AbstractShare { |
17
|
|
|
/** |
18
|
|
|
* @var Server $server |
19
|
|
|
*/ |
20
|
|
|
private $server; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var string $name |
24
|
|
|
*/ |
25
|
|
|
private $name; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var Connection $connection |
29
|
|
|
*/ |
30
|
|
|
public $connection; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @var \Icewind\SMB\Parser |
34
|
|
|
*/ |
35
|
|
|
protected $parser; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @var \Icewind\SMB\System |
39
|
|
|
*/ |
40
|
|
|
private $system; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @param Server $server |
44
|
|
|
* @param string $name |
45
|
|
|
*/ |
46
|
|
|
public function __construct($server, $name) { |
47
|
|
|
parent::__construct(); |
48
|
|
|
$this->server = $server; |
49
|
|
|
$this->name = $name; |
50
|
|
|
$this->system = new System(); |
51
|
|
|
$this->parser = new Parser(new TimeZoneProvider($this->server->getHost(), $this->system)); |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
protected function getConnection() { |
55
|
|
|
$workgroupArgument = ($this->server->getWorkgroup()) ? ' -W ' . escapeshellarg($this->server->getWorkgroup()) : ''; |
56
|
|
|
$command = sprintf('stdbuf -o0 %s %s --authentication-file=%s %s', |
57
|
|
|
$this->system->getSmbclientPath(), |
58
|
|
|
$workgroupArgument, |
59
|
|
|
System::getFD(3), |
60
|
|
|
escapeshellarg('//' . $this->server->getHost() . '/' . $this->name) |
61
|
|
|
); |
62
|
|
|
$connection = new Connection($command); |
63
|
|
|
$connection->writeAuthentication($this->server->getUser(), $this->server->getPassword()); |
64
|
|
|
if (!$connection->isValid()) { |
65
|
|
|
throw new ConnectionException(); |
66
|
|
|
} |
67
|
|
|
return $connection; |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @throws \Icewind\SMB\Exception\ConnectionException |
72
|
|
|
* @throws \Icewind\SMB\Exception\AuthenticationException |
73
|
|
|
* @throws \Icewind\SMB\Exception\InvalidHostException |
74
|
|
|
*/ |
75
|
|
|
protected function connect() { |
76
|
|
|
if ($this->connection and $this->connection->isValid()) { |
77
|
|
|
return; |
78
|
|
|
} |
79
|
|
|
$this->connection = $this->getConnection(); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
protected function reconnect() { |
83
|
|
|
$this->connection->reconnect(); |
84
|
|
|
$this->connection->writeAuthentication($this->server->getUser(), $this->server->getPassword()); |
85
|
|
|
if (!$this->connection->isValid()) { |
86
|
|
|
throw new ConnectionException(); |
87
|
|
|
} |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Get the name of the share |
92
|
|
|
* |
93
|
|
|
* @return string |
94
|
|
|
*/ |
95
|
|
|
public function getName() { |
96
|
|
|
return $this->name; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
protected function simpleCommand($command, $path) { |
100
|
|
|
$path = $this->escapePath($path); |
101
|
|
|
$cmd = $command . ' ' . $path; |
102
|
|
|
$output = $this->execute($cmd); |
103
|
|
|
return $this->parseOutput($output, $path); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* List the content of a remote folder |
108
|
|
|
* |
109
|
|
|
* @param $path |
110
|
|
|
* @return \Icewind\SMB\IFileInfo[] |
111
|
|
|
* |
112
|
|
|
* @throws \Icewind\SMB\Exception\NotFoundException |
113
|
|
|
* @throws \Icewind\SMB\Exception\InvalidTypeException |
114
|
|
|
*/ |
115
|
|
|
public function dir($path) { |
116
|
|
|
$escapedPath = $this->escapePath($path); |
117
|
|
|
$output = $this->execute('cd ' . $escapedPath); |
118
|
|
|
//check output for errors |
119
|
|
|
$this->parseOutput($output, $path); |
120
|
|
|
$output = $this->execute('dir'); |
121
|
|
|
$this->execute('cd /'); |
122
|
|
|
|
123
|
|
|
return $this->parser->parseDir($output, $path); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* @param string $path |
128
|
|
|
* @return \Icewind\SMB\IFileInfo[] |
129
|
|
|
*/ |
130
|
|
|
public function stat($path) { |
131
|
|
|
$escapedPath = $this->escapePath($path); |
132
|
|
|
$output = $this->execute('allinfo ' . $escapedPath); |
133
|
|
|
// Windows and non Windows Fileserver may respond different |
134
|
|
|
// to the allinfo command for directories. If the result is a single |
135
|
|
|
// line = error line, redo it with a different allinfo parameter |
136
|
|
|
if ($escapedPath == '""' && count($output) < 2) { |
137
|
|
|
$output = $this->execute('allinfo ' . '"."'); |
138
|
|
|
} |
139
|
|
|
if (count($output) < 3) { |
140
|
|
|
$this->parseOutput($output, $path); |
141
|
|
|
} |
142
|
|
|
$stat = $this->parser->parseStat($output); |
143
|
|
|
return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], $stat['mode']); |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Create a folder on the share |
148
|
|
|
* |
149
|
|
|
* @param string $path |
150
|
|
|
* @return bool |
151
|
|
|
* |
152
|
|
|
* @throws \Icewind\SMB\Exception\NotFoundException |
153
|
|
|
* @throws \Icewind\SMB\Exception\AlreadyExistsException |
154
|
|
|
*/ |
155
|
|
|
public function mkdir($path) { |
156
|
|
|
return $this->simpleCommand('mkdir', $path); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* Remove a folder on the share |
161
|
|
|
* |
162
|
|
|
* @param string $path |
163
|
|
|
* @return bool |
164
|
|
|
* |
165
|
|
|
* @throws \Icewind\SMB\Exception\NotFoundException |
166
|
|
|
* @throws \Icewind\SMB\Exception\InvalidTypeException |
167
|
|
|
*/ |
168
|
|
|
public function rmdir($path) { |
169
|
|
|
return $this->simpleCommand('rmdir', $path); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Delete a file on the share |
174
|
|
|
* |
175
|
|
|
* @param string $path |
176
|
|
|
* @param bool $secondTry |
177
|
|
|
* @return bool |
178
|
|
|
* @throws InvalidTypeException |
179
|
|
|
* @throws NotFoundException |
180
|
|
|
* @throws \Exception |
181
|
|
|
*/ |
182
|
|
|
public function del($path, $secondTry = false) { |
183
|
|
|
//del return a file not found error when trying to delete a folder |
184
|
|
|
//we catch it so we can check if $path doesn't exist or is of invalid type |
185
|
|
|
try { |
186
|
|
|
return $this->simpleCommand('del', $path); |
187
|
|
|
} catch (NotFoundException $e) { |
188
|
|
|
//no need to do anything with the result, we just check if this throws the not found error |
189
|
|
|
try { |
190
|
|
|
$this->simpleCommand('ls', $path); |
191
|
|
|
} catch (NotFoundException $e2) { |
192
|
|
|
throw $e; |
193
|
|
|
} catch (\Exception $e2) { |
194
|
|
|
throw new InvalidTypeException($path); |
195
|
|
|
} |
196
|
|
|
throw $e; |
197
|
|
|
} catch (FileInUseException $e) { |
198
|
|
|
if ($secondTry) { |
199
|
|
|
throw $e; |
200
|
|
|
} |
201
|
|
|
$this->reconnect(); |
202
|
|
|
return $this->del($path, true); |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* Rename a remote file |
208
|
|
|
* |
209
|
|
|
* @param string $from |
210
|
|
|
* @param string $to |
211
|
|
|
* @return bool |
212
|
|
|
* |
213
|
|
|
* @throws \Icewind\SMB\Exception\NotFoundException |
214
|
|
|
* @throws \Icewind\SMB\Exception\AlreadyExistsException |
215
|
|
|
*/ |
216
|
|
View Code Duplication |
public function rename($from, $to) { |
|
|
|
|
217
|
|
|
$path1 = $this->escapePath($from); |
218
|
|
|
$path2 = $this->escapePath($to); |
219
|
|
|
$cmd = 'rename ' . $path1 . ' ' . $path2; |
220
|
|
|
$output = $this->execute($cmd); |
221
|
|
|
return $this->parseOutput($output, $to); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Upload a local file |
226
|
|
|
* |
227
|
|
|
* @param string $source local file |
228
|
|
|
* @param string $target remove file |
229
|
|
|
* @return bool |
230
|
|
|
* |
231
|
|
|
* @throws \Icewind\SMB\Exception\NotFoundException |
232
|
|
|
* @throws \Icewind\SMB\Exception\InvalidTypeException |
233
|
|
|
*/ |
234
|
|
View Code Duplication |
public function put($source, $target) { |
|
|
|
|
235
|
|
|
$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping |
236
|
|
|
$path2 = $this->escapePath($target); |
237
|
|
|
$output = $this->execute('put ' . $path1 . ' ' . $path2); |
238
|
|
|
return $this->parseOutput($output, $target); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Download a remote file |
243
|
|
|
* |
244
|
|
|
* @param string $source remove file |
245
|
|
|
* @param string $target local file |
246
|
|
|
* @return bool |
247
|
|
|
* |
248
|
|
|
* @throws \Icewind\SMB\Exception\NotFoundException |
249
|
|
|
* @throws \Icewind\SMB\Exception\InvalidTypeException |
250
|
|
|
*/ |
251
|
|
View Code Duplication |
public function get($source, $target) { |
|
|
|
|
252
|
|
|
$path1 = $this->escapePath($source); |
253
|
|
|
$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping |
254
|
|
|
$output = $this->execute('get ' . $path1 . ' ' . $path2); |
255
|
|
|
return $this->parseOutput($output, $source); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Open a readable stream to a remote file |
260
|
|
|
* |
261
|
|
|
* @param string $source |
262
|
|
|
* @return resource a read only stream with the contents of the remote file |
263
|
|
|
* |
264
|
|
|
* @throws \Icewind\SMB\Exception\NotFoundException |
265
|
|
|
* @throws \Icewind\SMB\Exception\InvalidTypeException |
266
|
|
|
*/ |
267
|
|
|
public function read($source) { |
268
|
|
|
$source = $this->escapePath($source); |
269
|
|
|
// since returned stream is closed by the caller we need to create a new instance |
270
|
|
|
// since we can't re-use the same file descriptor over multiple calls |
271
|
|
|
$workgroupArgument = ($this->server->getWorkgroup()) ? ' -W ' . escapeshellarg($this->server->getWorkgroup()) : ''; |
272
|
|
|
$command = sprintf('%s %s --authentication-file=%s %s', |
273
|
|
|
$this->system->getSmbclientPath(), |
274
|
|
|
$workgroupArgument, |
275
|
|
|
System::getFD(3), |
276
|
|
|
escapeshellarg('//' . $this->server->getHost() . '/' . $this->name) |
277
|
|
|
); |
278
|
|
|
$connection = new Connection($command); |
279
|
|
|
$connection->writeAuthentication($this->server->getUser(), $this->server->getPassword()); |
280
|
|
|
$connection->write('get ' . $source . ' ' . System::getFD(5)); |
281
|
|
|
$connection->write('exit'); |
282
|
|
|
$fh = $connection->getFileOutputStream(); |
283
|
|
|
stream_context_set_option($fh, 'file', 'connection', $connection); |
284
|
|
|
return $fh; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Open a writable stream to a remote file |
289
|
|
|
* |
290
|
|
|
* @param string $target |
291
|
|
|
* @return resource a write only stream to upload a remote file |
292
|
|
|
* |
293
|
|
|
* @throws \Icewind\SMB\Exception\NotFoundException |
294
|
|
|
* @throws \Icewind\SMB\Exception\InvalidTypeException |
295
|
|
|
*/ |
296
|
|
|
public function write($target) { |
297
|
|
|
$target = $this->escapePath($target); |
298
|
|
|
// since returned stream is closed by the caller we need to create a new instance |
299
|
|
|
// since we can't re-use the same file descriptor over multiple calls |
300
|
|
|
$workgroupArgument = ($this->server->getWorkgroup()) ? ' -W ' . escapeshellarg($this->server->getWorkgroup()) : ''; |
301
|
|
|
$command = sprintf('%s %s --authentication-file=%s %s', |
302
|
|
|
$this->system->getSmbclientPath(), |
303
|
|
|
$workgroupArgument, |
304
|
|
|
System::getFD(3), |
305
|
|
|
escapeshellarg('//' . $this->server->getHost() . '/' . $this->name) |
306
|
|
|
); |
307
|
|
|
$connection = new Connection($command); |
308
|
|
|
$connection->writeAuthentication($this->server->getUser(), $this->server->getPassword()); |
309
|
|
|
$fh = $connection->getFileInputStream(); |
310
|
|
|
|
311
|
|
|
$connection->write('put ' . System::getFD(4) . ' ' . $target); |
312
|
|
|
$connection->write('exit'); |
313
|
|
|
|
314
|
|
|
// use a close callback to ensure the upload is finished before continuing |
315
|
|
|
// this also serves as a way to keep the connection in scope |
316
|
|
|
return CallbackWrapper::wrap($fh, null, null, function () use ($connection, $target) { |
317
|
|
|
$connection->close(false); // dont terminate, give the upload some time |
318
|
|
|
}); |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* @param string $path |
323
|
|
|
* @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL |
324
|
|
|
* @return mixed |
325
|
|
|
*/ |
326
|
|
|
public function setMode($path, $mode) { |
327
|
|
|
$modeString = ''; |
328
|
|
|
$modeMap = array( |
329
|
|
|
FileInfo::MODE_READONLY => 'r', |
330
|
|
|
FileInfo::MODE_HIDDEN => 'h', |
331
|
|
|
FileInfo::MODE_ARCHIVE => 'a', |
332
|
|
|
FileInfo::MODE_SYSTEM => 's' |
333
|
|
|
); |
334
|
|
|
foreach ($modeMap as $modeByte => $string) { |
335
|
|
|
if ($mode & $modeByte) { |
336
|
|
|
$modeString .= $string; |
337
|
|
|
} |
338
|
|
|
} |
339
|
|
|
$path = $this->escapePath($path); |
340
|
|
|
|
341
|
|
|
// first reset the mode to normal |
342
|
|
|
$cmd = 'setmode ' . $path . ' -rsha'; |
343
|
|
|
$output = $this->execute($cmd); |
344
|
|
|
$this->parseOutput($output, $path); |
345
|
|
|
|
346
|
|
|
// then set the modes we want |
347
|
|
|
$cmd = 'setmode ' . $path . ' ' . $modeString; |
348
|
|
|
$output = $this->execute($cmd); |
349
|
|
|
return $this->parseOutput($output, $path); |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
/** |
353
|
|
|
* @param string $path |
354
|
|
|
* @param callable $callback callable which will be called for each received change |
355
|
|
|
* @return mixed |
356
|
|
|
*/ |
357
|
|
|
public function notify($path, callable $callback) { |
358
|
|
|
$connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process |
359
|
|
|
$command = 'notify ' . $this->escapePath($path); |
360
|
|
|
$connection->write($command . PHP_EOL); |
361
|
|
|
$connection->read(function ($line) use ($callback, $path) { |
362
|
|
|
$code = (int)substr($line, 0, 4); |
363
|
|
|
$subPath = substr($line, 5); |
364
|
|
|
if ($path === '') { |
365
|
|
|
return $callback($code, $subPath); |
366
|
|
|
} else { |
367
|
|
|
return $callback($code, $path . '/' . $subPath); |
368
|
|
|
} |
369
|
|
|
}); |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
/** |
373
|
|
|
* @param string $command |
374
|
|
|
* @return array |
375
|
|
|
*/ |
376
|
|
|
protected function execute($command) { |
377
|
|
|
$this->connect(); |
378
|
|
|
$this->connection->write($command . PHP_EOL); |
379
|
|
|
$output = $this->connection->read(); |
380
|
|
|
return $output; |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
/** |
384
|
|
|
* check output for errors |
385
|
|
|
* |
386
|
|
|
* @param string[] $lines |
387
|
|
|
* @param string $path |
388
|
|
|
* |
389
|
|
|
* @throws NotFoundException |
390
|
|
|
* @throws \Icewind\SMB\Exception\AlreadyExistsException |
391
|
|
|
* @throws \Icewind\SMB\Exception\AccessDeniedException |
392
|
|
|
* @throws \Icewind\SMB\Exception\NotEmptyException |
393
|
|
|
* @throws \Icewind\SMB\Exception\InvalidTypeException |
394
|
|
|
* @throws \Icewind\SMB\Exception\Exception |
395
|
|
|
* @return bool |
396
|
|
|
*/ |
397
|
|
|
protected function parseOutput($lines, $path = '') { |
398
|
|
|
return $this->parser->checkForError($lines, $path); |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
/** |
402
|
|
|
* @param string $string |
403
|
|
|
* @return string |
404
|
|
|
*/ |
405
|
|
|
protected function escape($string) { |
406
|
|
|
return escapeshellarg($string); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* @param string $path |
411
|
|
|
* @return string |
412
|
|
|
*/ |
413
|
|
|
protected function escapePath($path) { |
414
|
|
|
$this->verifyPath($path); |
415
|
|
|
if ($path === '/') { |
416
|
|
|
$path = ''; |
417
|
|
|
} |
418
|
|
|
$path = str_replace('/', '\\', $path); |
419
|
|
|
$path = str_replace('"', '^"', $path); |
420
|
|
|
return '"' . $path . '"'; |
421
|
|
|
} |
422
|
|
|
|
423
|
|
|
/** |
424
|
|
|
* @param string $path |
425
|
|
|
* @return string |
426
|
|
|
*/ |
427
|
|
|
protected function escapeLocalPath($path) { |
428
|
|
|
$path = str_replace('"', '\"', $path); |
429
|
|
|
return '"' . $path . '"'; |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
public function __destruct() { |
433
|
|
|
unset($this->connection); |
434
|
|
|
} |
435
|
|
|
} |
436
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.