Completed
Push — master ( 1bf43b...c1afd7 )
by Robin
01:56
created

Share::simpleCommand()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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