Completed
Push — stable2 ( 10fadc...4d0e89 )
by Robin
07:41
created

Share::stat()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.1929

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 16
cts 19
cp 0.8421
rs 8.5386
c 0
b 0
f 0
cc 7
nc 9
nop 1
crap 7.1929
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\DependencyException;
12
use Icewind\SMB\Exception\FileInUseException;
13
use Icewind\SMB\Exception\InvalidTypeException;
14
use Icewind\SMB\Exception\NotFoundException;
15
use Icewind\Streams\CallbackWrapper;
16
17
class Share extends AbstractShare {
18
	/**
19
	 * @var Server $server
20
	 */
21
	private $server;
22
23
	/**
24
	 * @var string $name
25
	 */
26
	private $name;
27
28
	/**
29
	 * @var Connection $connection
30
	 */
31
	public $connection;
32
33
	/**
34
	 * @var \Icewind\SMB\Parser
35
	 */
36
	protected $parser;
37
38
	/**
39
	 * @var \Icewind\SMB\System
40
	 */
41
	private $system;
42
43
	/**
44
	 * @param Server $server
45
	 * @param string $name
46
	 * @param System $system
47
	 */
48 1028
	public function __construct($server, $name, System $system = null) {
49 1028
		parent::__construct();
50 1028
		$this->server = $server;
51 1028
		$this->name = $name;
52 1028
		$this->system = (!is_null($system)) ? $system : new System();
53 1028
		$this->parser = new Parser(new TimeZoneProvider($this->server->getHost(), $this->system));
54 1028
	}
55
56 1024
	protected function getConnection() {
57 1024
		$workgroupArgument = ($this->server->getWorkgroup()) ? ' -W ' . escapeshellarg($this->server->getWorkgroup()) : '';
58 1024
		$smbClientPath = $this->system->getSmbclientPath();
59 1024
		if (!$smbClientPath) {
60 4
			throw new DependencyException('Can\'t find smbclient binary in path');
61
		}
62 1024
		$command = sprintf('%s%s %s --authentication-file=%s %s',
63 1024
			$this->system->hasStdBuf() ? 'stdbuf -o0 ' : '',
64 1024
			$this->system->getSmbclientPath(),
65 1024
			$workgroupArgument,
66 1024
			System::getFD(3),
67 1024
			escapeshellarg('//' . $this->server->getHost() . '/' . $this->name)
68 512
		);
69 1024
		$connection = new Connection($command, $this->parser);
70 1024
		$connection->writeAuthentication($this->server->getUser(), $this->server->getPassword());
71 1024
		$connection->connect();
72 1024
		if (!$connection->isValid()) {
73
			throw new ConnectionException($connection->readLine());
74
		}
75
		// some versions of smbclient add a help message in first of the first prompt
76 1024
		$connection->clearTillPrompt();
77 1024
		return $connection;
78
	}
79
80
	/**
81
	 * @throws \Icewind\SMB\Exception\ConnectionException
82
	 * @throws \Icewind\SMB\Exception\AuthenticationException
83
	 * @throws \Icewind\SMB\Exception\InvalidHostException
84
	 */
85 1020
	protected function connect() {
86 1020
		if ($this->connection and $this->connection->isValid()) {
87 1020
			return;
88
		}
89 1020
		$this->connection = $this->getConnection();
90 1020
	}
91
92 4
	protected function reconnect() {
93 4
		$this->connection->reconnect();
94 4
		if (!$this->connection->isValid()) {
95
			throw new ConnectionException();
96
		}
97 4
	}
98
99
	/**
100
	 * Get the name of the share
101
	 *
102
	 * @return string
103
	 */
104 8
	public function getName() {
105 8
		return $this->name;
106
	}
107
108 1020
	protected function simpleCommand($command, $path) {
109 1020
		$escapedPath = $this->escapePath($path);
110 1020
		$cmd = $command . ' ' . $escapedPath;
111 1020
		$output = $this->execute($cmd);
112 1020
		return $this->parseOutput($output, $path);
113
	}
114
115
	/**
116
	 * List the content of a remote folder
117
	 *
118
	 * @param $path
119
	 * @return \Icewind\SMB\IFileInfo[]
120
	 *
121
	 * @throws \Icewind\SMB\Exception\NotFoundException
122
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
123
	 */
124 1004
	public function dir($path) {
125 1004
		$escapedPath = $this->escapePath($path);
126 1004
		$output = $this->execute('cd ' . $escapedPath);
127
		//check output for errors
128 1004
		$this->parseOutput($output, $path);
129 1004
		$output = $this->execute('dir');
130
131 1004
		$this->execute('cd /');
132
133 1004
		return $this->parser->parseDir($output, $path);
134
	}
135
136
	/**
137
	 * @param string $path
138
	 * @return \Icewind\SMB\IFileInfo
139
	 */
140 132
	public function stat($path) {
141
		// some windows server setups don't seem to like the allinfo command
142
		// use the dir command instead to get the file info where possible
143 132
		if ($path !== "" && $path !== "/") {
144 128
			$parent = dirname($path);
145 128
			$dir = $this->dir($parent);
146
			$file = array_values(array_filter($dir, function(IFileInfo $info) use ($path) {
147 124
				return $info->getPath() === $path;
148 128
			}));
149 128
			if ($file) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $file of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
150 88
				return $file[0];
151
			}
152 20
		}
153
154 44
		$escapedPath = $this->escapePath($path);
155 8
		$output = $this->execute('allinfo ' . $escapedPath);
156
		// Windows and non Windows Fileserver may respond different
157
		// to the allinfo command for directories. If the result is a single
158
		// line = error line, redo it with a different allinfo parameter
159 8
		if ($escapedPath == '""' && count($output) < 2) {
160
			$output = $this->execute('allinfo ' . '"."');
161
		}
162 8
		if (count($output) < 3) {
163 4
			$this->parseOutput($output, $path);
164
		}
165 4
		$stat = $this->parser->parseStat($output);
166 4
		return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], $stat['mode']);
167
	}
168
169
	/**
170
	 * Create a folder on the share
171
	 *
172
	 * @param string $path
173
	 * @return bool
174
	 *
175
	 * @throws \Icewind\SMB\Exception\NotFoundException
176
	 * @throws \Icewind\SMB\Exception\AlreadyExistsException
177
	 */
178 1008
	public function mkdir($path) {
179 1008
		return $this->simpleCommand('mkdir', $path);
180
	}
181
182
	/**
183
	 * Remove a folder on the share
184
	 *
185
	 * @param string $path
186
	 * @return bool
187
	 *
188
	 * @throws \Icewind\SMB\Exception\NotFoundException
189
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
190
	 */
191 1008
	public function rmdir($path) {
192 1008
		return $this->simpleCommand('rmdir', $path);
193
	}
194
195
	/**
196
	 * Delete a file on the share
197
	 *
198
	 * @param string $path
199
	 * @param bool $secondTry
200
	 * @return bool
201
	 * @throws InvalidTypeException
202
	 * @throws NotFoundException
203
	 * @throws \Exception
204
	 */
205 524
	public function del($path, $secondTry = false) {
206
		//del return a file not found error when trying to delete a folder
207
		//we catch it so we can check if $path doesn't exist or is of invalid type
208
		try {
209 524
			return $this->simpleCommand('del', $path);
210 48
		} catch (NotFoundException $e) {
211
			//no need to do anything with the result, we just check if this throws the not found error
212
			try {
213 8
				$this->simpleCommand('ls', $path);
214 8
			} catch (NotFoundException $e2) {
215 4
				throw $e;
216 4
			} catch (\Exception $e2) {
217 4
				throw new InvalidTypeException($path);
218
			}
219
			throw $e;
220 40
		} catch (FileInUseException $e) {
221 4
			if ($secondTry) {
222
				throw $e;
223
			}
224 4
			$this->reconnect();
225 4
			return $this->del($path, true);
226
		}
227
	}
228
229
	/**
230
	 * Rename a remote file
231
	 *
232
	 * @param string $from
233
	 * @param string $to
234
	 * @return bool
235
	 *
236
	 * @throws \Icewind\SMB\Exception\NotFoundException
237
	 * @throws \Icewind\SMB\Exception\AlreadyExistsException
238
	 */
239 120 View Code Duplication
	public function rename($from, $to) {
240 120
		$path1 = $this->escapePath($from);
241 84
		$path2 = $this->escapePath($to);
242 84
		$output = $this->execute('rename ' . $path1 . ' ' . $path2);
243 84
		return $this->parseOutput($output, $to);
244
	}
245
246
	/**
247
	 * Upload a local file
248
	 *
249
	 * @param string $source local file
250
	 * @param string $target remove file
251
	 * @return bool
252
	 *
253
	 * @throws \Icewind\SMB\Exception\NotFoundException
254
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
255
	 */
256 460 View Code Duplication
	public function put($source, $target) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
257 460
		$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
258 460
		$path2 = $this->escapePath($target);
259 424
		$output = $this->execute('put ' . $path1 . ' ' . $path2);
260 424
		return $this->parseOutput($output, $target);
261
	}
262
263
	/**
264
	 * Download a remote file
265
	 *
266
	 * @param string $source remove file
267
	 * @param string $target local file
268
	 * @return bool
269
	 *
270
	 * @throws \Icewind\SMB\Exception\NotFoundException
271
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
272
	 */
273 176 View Code Duplication
	public function get($source, $target) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
274 176
		$path1 = $this->escapePath($source);
275 140
		$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping
276 140
		$output = $this->execute('get ' . $path1 . ' ' . $path2);
277 140
		return $this->parseOutput($output, $source);
278
	}
279
280
	/**
281
	 * Open a readable stream to a remote file
282
	 *
283
	 * @param string $source
284
	 * @return resource a read only stream with the contents of the remote file
285
	 *
286
	 * @throws \Icewind\SMB\Exception\NotFoundException
287
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
288
	 */
289 100
	public function read($source) {
290 100
		$source = $this->escapePath($source);
291
		// since returned stream is closed by the caller we need to create a new instance
292
		// since we can't re-use the same file descriptor over multiple calls
293 64
		$connection = $this->getConnection();
294
295 64
		$connection->write('get ' . $source . ' ' . System::getFD(5));
296 64
		$connection->write('exit');
297 64
		$fh = $connection->getFileOutputStream();
298 64
		stream_context_set_option($fh, 'file', 'connection', $connection);
299 64
		return $fh;
300
	}
301
302
	/**
303
	 * Open a writable stream to a remote file
304
	 *
305
	 * @param string $target
306
	 * @return resource a write only stream to upload a remote file
307
	 *
308
	 * @throws \Icewind\SMB\Exception\NotFoundException
309
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
310
	 */
311 100
	public function write($target) {
312 100
		$target = $this->escapePath($target);
313
		// since returned stream is closed by the caller we need to create a new instance
314
		// since we can't re-use the same file descriptor over multiple calls
315 64
		$connection = $this->getConnection();
316
317 64
		$fh = $connection->getFileInputStream();
318 64
		$connection->write('put ' . System::getFD(4) . ' ' . $target);
319 64
		$connection->write('exit');
320
321
		// use a close callback to ensure the upload is finished before continuing
322
		// this also serves as a way to keep the connection in scope
323 64
		return CallbackWrapper::wrap($fh, null, null, function () use ($connection, $target) {
324 64
			$connection->close(false); // dont terminate, give the upload some time
325 64
		});
326
	}
327
328
	/**
329
	 * @param string $path
330
	 * @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
331
	 * @return mixed
332
	 */
333 32
	public function setMode($path, $mode) {
334 32
		$modeString = '';
335
		$modeMap = array(
336 32
			FileInfo::MODE_READONLY => 'r',
337 16
			FileInfo::MODE_HIDDEN   => 'h',
338 16
			FileInfo::MODE_ARCHIVE  => 'a',
339 16
			FileInfo::MODE_SYSTEM   => 's'
340 16
		);
341 32
		foreach ($modeMap as $modeByte => $string) {
342 32
			if ($mode & $modeByte) {
343 32
				$modeString .= $string;
344 16
			}
345 16
		}
346 32
		$path = $this->escapePath($path);
347
348
		// first reset the mode to normal
349 32
		$cmd = 'setmode ' . $path . ' -rsha';
350 32
		$output = $this->execute($cmd);
351 32
		$this->parseOutput($output, $path);
352
353 32
		if ($mode !== FileInfo::MODE_NORMAL) {
354
			// then set the modes we want
355 32
			$cmd = 'setmode ' . $path . ' ' . $modeString;
356 32
			$output = $this->execute($cmd);
357 32
			return $this->parseOutput($output, $path);
358
		} else {
359 32
			return true;
360
		}
361
	}
362
363
	/**
364
	 * @param string $path
365
	 * @return INotifyHandler
366
	 * @throws ConnectionException
367
	 * @throws DependencyException
368
	 */
369 20
	public function notify($path) {
370 20
		if (!$this->system->hasStdBuf()) { //stdbuf is required to disable smbclient's output buffering
371
			throw new DependencyException('stdbuf is required for usage of the notify command');
372
		}
373 20
		$connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process
374 20
		$command = 'notify ' . $this->escapePath($path);
375 20
		$connection->write($command . PHP_EOL);
376 20
		return new NotifyHandler($connection, $path);
377
	}
378
379
	/**
380
	 * @param string $command
381
	 * @return array
382
	 */
383 1020
	protected function execute($command) {
384 1020
		$this->connect();
385 1020
		$this->connection->write($command . PHP_EOL);
386 1020
		return $this->connection->read();
387
	}
388
389
	/**
390
	 * check output for errors
391
	 *
392
	 * @param string[] $lines
393
	 * @param string $path
394
	 *
395
	 * @throws NotFoundException
396
	 * @throws \Icewind\SMB\Exception\AlreadyExistsException
397
	 * @throws \Icewind\SMB\Exception\AccessDeniedException
398
	 * @throws \Icewind\SMB\Exception\NotEmptyException
399
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
400
	 * @throws \Icewind\SMB\Exception\Exception
401
	 * @return bool
402
	 */
403 1020
	protected function parseOutput($lines, $path = '') {
404 1020
		if (count($lines) === 0) {
405 1020
			return true;
406
		} else {
407 80
			$this->parser->checkForError($lines, $path);
408
			return false;
409
		}
410
	}
411
412
	/**
413
	 * @param string $string
414
	 * @return string
415
	 */
416
	protected function escape($string) {
417
		return escapeshellarg($string);
418
	}
419
420
	/**
421
	 * @param string $path
422
	 * @return string
423
	 */
424 1024
	protected function escapePath($path) {
425 1024
		$this->verifyPath($path);
426 1024
		if ($path === '/') {
427 4
			$path = '';
428 2
		}
429 1024
		$path = str_replace('/', '\\', $path);
430 1024
		$path = str_replace('"', '^"', $path);
431 1024
		$path = ltrim($path, '\\');
432 1024
		return '"' . $path . '"';
433
	}
434
435
	/**
436
	 * @param string $path
437
	 * @return string
438
	 */
439 532
	protected function escapeLocalPath($path) {
440 532
		$path = str_replace('"', '\"', $path);
441 532
		return '"' . $path . '"';
442
	}
443
444 1028
	public function __destruct() {
445 1028
		unset($this->connection);
446 1028
	}
447
}
448