Completed
Pull Request — master (#70)
by Raffael
10:29
created

Share::append()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
c 0
b 0
f 0
ccs 1
cts 1
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
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\Wrapped;
9
10
use Icewind\SMB\AbstractShare;
11
use Icewind\SMB\Exception\ConnectionException;
12
use Icewind\SMB\Exception\DependencyException;
13
use Icewind\SMB\Exception\FileInUseException;
14
use Icewind\SMB\Exception\InvalidTypeException;
15
use Icewind\SMB\Exception\NotFoundException;
16
use Icewind\SMB\IFileInfo;
17
use Icewind\SMB\INotifyHandler;
18
use Icewind\SMB\IServer;
19
use Icewind\SMB\ISystem;
20
use Icewind\Streams\CallbackWrapper;
21
use Icewind\SMB\Native\NativeShare;
22
use Icewind\SMB\Native\NativeServer;
23
24
class Share extends AbstractShare {
25
	/**
26
	 * @var IServer $server
27
	 */
28
	private $server;
29
30
	/**
31
	 * @var string $name
32
	 */
33
	private $name;
34
35
	/**
36
	 * @var Connection $connection
37
	 */
38
	public $connection;
39
40
	/**
41
	 * @var Parser
42
	 */
43
	protected $parser;
44
45
	/**
46
	 * @var ISystem
47
	 */
48
	private $system;
49
50
	const MODE_MAP = [
51
		FileInfo::MODE_READONLY => 'r',
52
		FileInfo::MODE_HIDDEN   => 'h',
53
		FileInfo::MODE_ARCHIVE  => 'a',
54
		FileInfo::MODE_SYSTEM   => 's'
55
	];
56
57
	/**
58
	 * @param IServer $server
59
	 * @param string $name
60 768
	 * @param ISystem $system
61 768
	 */
62 768
	public function __construct(IServer $server, $name, ISystem $system) {
63 768
		parent::__construct();
64 768
		$this->server = $server;
65 768
		$this->name = $name;
66 768
		$this->system = $system;
67
		$this->parser = new Parser($server->getTimeZone());
68 765
	}
69 765
70 765
	private function getAuthFileArgument() {
71
		if ($this->server->getAuth()->getUsername()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->server->getAuth()->getUsername() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
72
			return '--authentication-file=' . $this->system->getFD(3);
73
		} else {
74
			return '';
75
		}
76 765
	}
77 765
78 765
	protected function getConnection() {
79 765
		$command = sprintf('%s%s -t %s %s %s %s',
80 765
			$this->system->getStdBufPath() ? $this->system->getStdBufPath() . ' -o0 ' : '',
81 765
			$this->system->getSmbclientPath(),
82 765
			$this->server->getOptions()->getTimeout(),
83 765
			$this->getAuthFileArgument(),
84
			$this->server->getAuth()->getExtraCommandLineArguments(),
85 765
			escapeshellarg('//' . $this->server->getHost() . '/' . $this->name)
86 765
		);
87 765
		$connection = new Connection($command, $this->parser);
88 765
		$connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword());
89
		$connection->connect();
90
		if (!$connection->isValid()) {
91
			throw new ConnectionException($connection->readLine());
92 765
		}
93 765
		// some versions of smbclient add a help message in first of the first prompt
94
		$connection->clearTillPrompt();
95
		return $connection;
96
	}
97
98
	/**
99
	 * @throws \Icewind\SMB\Exception\ConnectionException
100
	 * @throws \Icewind\SMB\Exception\AuthenticationException
101 762
	 * @throws \Icewind\SMB\Exception\InvalidHostException
102 762
	 */
103 762
	protected function connect() {
104
		if ($this->connection and $this->connection->isValid()) {
105 762
			return;
106 762
		}
107
		$this->connection = $this->getConnection();
108 3
	}
109 3
110 3
	protected function reconnect() {
111
		$this->connection->reconnect();
112
		if (!$this->connection->isValid()) {
113 3
			throw new ConnectionException();
114
		}
115
	}
116
117
	/**
118
	 * Get the name of the share
119
	 *
120 6
	 * @return string
121 6
	 */
122
	public function getName() {
123
		return $this->name;
124 762
	}
125 762
126 762
	protected function simpleCommand($command, $path) {
127 762
		$escapedPath = $this->escapePath($path);
128 762
		$cmd = $command . ' ' . $escapedPath;
129
		$output = $this->execute($cmd);
130
		return $this->parseOutput($output, $path);
131
	}
132
133
	/**
134
	 * List the content of a remote folder
135
	 *
136
	 * @param $path
137
	 * @return \Icewind\SMB\IFileInfo[]
138
	 *
139
	 * @throws \Icewind\SMB\Exception\NotFoundException
140 750
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
141 750
	 */
142 750
	public function dir($path) {
143
		$escapedPath = $this->escapePath($path);
144 750
		$output = $this->execute('cd ' . $escapedPath);
145 750
		//check output for errors
146
		$this->parseOutput($output, $path);
147 750
		$output = $this->execute('dir');
148
149 750
		$this->execute('cd /');
150
151
		return $this->parser->parseDir($output, $path);
152
	}
153
154
	/**
155
	 * @param string $path
156 99
	 * @return \Icewind\SMB\IFileInfo
157 99
	 */
158 72
	public function stat($path) {
159
		// some windows server setups don't seem to like the allinfo command
160
		// use the dir command instead to get the file info where possible
161
		if ($path !== "" && $path !== "/") {
162 72
			$parent = dirname($path);
163
			$dir = $this->dir($parent);
164
			$file = array_values(array_filter($dir, function(IFileInfo $info) use ($path) {
165 72
				return $info->getPath() === $path;
166 3
			}));
167
			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...
168 69
				return $file[0];
169 69
			}
170
		}
171
172
		$escapedPath = $this->escapePath($path);
173
		$output = $this->execute('allinfo ' . $escapedPath);
174
		// Windows and non Windows Fileserver may respond different
175
		// to the allinfo command for directories. If the result is a single
176
		// line = error line, redo it with a different allinfo parameter
177
		if ($escapedPath == '""' && count($output) < 2) {
178
			$output = $this->execute('allinfo ' . '"."');
179
		}
180
		if (count($output) < 3) {
181 753
			$this->parseOutput($output, $path);
182 753
		}
183
		$stat = $this->parser->parseStat($output);
184
		return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], $stat['mode']);
185
	}
186
187
	/**
188
	 * Create a folder on the share
189
	 *
190
	 * @param string $path
191
	 * @return bool
192
	 *
193
	 * @throws \Icewind\SMB\Exception\NotFoundException
194 753
	 * @throws \Icewind\SMB\Exception\AlreadyExistsException
195 753
	 */
196
	public function mkdir($path) {
197
		return $this->simpleCommand('mkdir', $path);
198
	}
199
200
	/**
201
	 * Remove a folder on the share
202
	 *
203
	 * @param string $path
204
	 * @return bool
205
	 *
206
	 * @throws \Icewind\SMB\Exception\NotFoundException
207
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
208 393
	 */
209
	public function rmdir($path) {
210
		return $this->simpleCommand('rmdir', $path);
211
	}
212 393
213 36
	/**
214
	 * Delete a file on the share
215
	 *
216 6
	 * @param string $path
217 6
	 * @param bool $secondTry
218 3
	 * @return bool
219 3
	 * @throws InvalidTypeException
220 3
	 * @throws NotFoundException
221
	 * @throws \Exception
222
	 */
223 30
	public function del($path, $secondTry = false) {
224 3
		//del return a file not found error when trying to delete a folder
225
		//we catch it so we can check if $path doesn't exist or is of invalid type
226
		try {
227 3
			return $this->simpleCommand('del', $path);
228 3
		} catch (NotFoundException $e) {
229
			//no need to do anything with the result, we just check if this throws the not found error
230
			try {
231
				$this->simpleCommand('ls', $path);
232
			} catch (NotFoundException $e2) {
233
				throw $e;
234
			} catch (\Exception $e2) {
235
				throw new InvalidTypeException($path);
236
			}
237
			throw $e;
238
		} catch (FileInUseException $e) {
239
			if ($secondTry) {
240
				throw $e;
241
			}
242 90
			$this->reconnect();
243 90
			return $this->del($path, true);
244 63
		}
245 63
	}
246 63
247
	/**
248
	 * Rename a remote file
249
	 *
250
	 * @param string $from
251
	 * @param string $to
252
	 * @return bool
253
	 *
254
	 * @throws \Icewind\SMB\Exception\NotFoundException
255
	 * @throws \Icewind\SMB\Exception\AlreadyExistsException
256
	 */
257 View Code Duplication
	public function rename($from, $to) {
258
		$path1 = $this->escapePath($from);
259 345
		$path2 = $this->escapePath($to);
260 345
		$output = $this->execute('rename ' . $path1 . ' ' . $path2);
261 345
		return $this->parseOutput($output, $to);
262 318
	}
263 318
264
	/**
265
	 * Upload a local file
266
	 *
267
	 * @param string $source local file
268
	 * @param string $target remove file
269
	 * @return bool
270
	 *
271
	 * @throws \Icewind\SMB\Exception\NotFoundException
272
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
273
	 */
274 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...
275
		$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
276 132
		$path2 = $this->escapePath($target);
277 132
		$output = $this->execute('put ' . $path1 . ' ' . $path2);
278 105
		return $this->parseOutput($output, $target);
279 105
	}
280 105
281
	/**
282
	 * Download a remote file
283
	 *
284
	 * @param string $source remove file
285
	 * @param string $target local file
286
	 * @return bool
287
	 *
288
	 * @throws \Icewind\SMB\Exception\NotFoundException
289
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
290
	 */
291 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...
292 75
		$path1 = $this->escapePath($source);
293 75
		$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping
294
		$output = $this->execute('get ' . $path1 . ' ' . $path2);
295
		return $this->parseOutput($output, $source);
296 48
	}
297
298 48
	/**
299 48
	 * Open a readable stream to a remote file
300 48
	 *
301 48
	 * @param string $source
302 48
	 * @return resource a read only stream with the contents of the remote file
303
	 *
304
	 * @throws \Icewind\SMB\Exception\NotFoundException
305
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
306
	 */
307
	public function read($source) {
308
		$source = $this->escapePath($source);
309
		// since returned stream is closed by the caller we need to create a new instance
310
		// since we can't re-use the same file descriptor over multiple calls
311
		$connection = $this->getConnection();
312
313
		$connection->write('get ' . $source . ' ' . $this->system->getFD(5));
314 75
		$connection->write('exit');
315 75
		$fh = $connection->getFileOutputStream();
316
		stream_context_set_option($fh, 'file', 'connection', $connection);
317
		return $fh;
318 48
	}
319
320 48
	/**
321 48
	 * Open a writable stream to a remote file
322 48
	 *
323
	 * @param string $target
324
	 * @return resource a write only stream to upload a remote file
325
	 *
326 48
	 * @throws \Icewind\SMB\Exception\NotFoundException
327 48
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
328 48
	 */
329
	public function write($target) {
330
		$target = $this->escapePath($target);
331
		// since returned stream is closed by the caller we need to create a new instance
332
		// since we can't re-use the same file descriptor over multiple calls
333
		$connection = $this->getConnection();
334
335
		$fh = $connection->getFileInputStream();
336 24
		$connection->write('put ' . $this->system->getFD(4) . ' ' . $target);
337 24
		$connection->write('exit');
338 24
339 24
		// use a close callback to ensure the upload is finished before continuing
340 24
		// this also serves as a way to keep the connection in scope
341
		return CallbackWrapper::wrap($fh, null, null, function () use ($connection, $target) {
342
			$connection->close(false); // dont terminate, give the upload some time
343 24
		});
344
	}
345
346 24
 	/**
0 ignored issues
show
Coding Style introduced by
There is some trailing whitespace on this line which should be avoided as per coding-style.
Loading history...
347 24
	 * Append to stream
348 24
	 * Note: smbclient does not support it so we use php-libsmbclient 
349
	 *
350 24
	 * @param string $target
351
 	 *
352 24
 	 * @throws \Icewind\SMB\Exception\DependencyException
353 24
	 * @throws \Icewind\SMB\Exception\NotFoundException
354 24
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
355
	 */
356 24
	public function append($target) {
357
		if (!NativeServer::available($this->server->getSystem())) {
358
			throw new DependencyException('php-libsmbclient not installed');
359
		}
360
361
 		$share = new NativeShare($this->server, $this->getName());
362
		return $share->append($target);
363
	}
364
365
	/**
366 15
	 * @param string $path
367 15
	 * @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
368
	 * @return mixed
369
	 */
370 15
	public function setMode($path, $mode) {
371 15
		$modeString = '';
372 15
		foreach (self::MODE_MAP as $modeByte => $string) {
373 15
			if ($mode & $modeByte) {
374
				$modeString .= $string;
375
			}
376
		}
377
		$path = $this->escapePath($path);
378
379
		// first reset the mode to normal
380 762
		$cmd = 'setmode ' . $path . ' -rsha';
381 762
		$output = $this->execute($cmd);
382 762
		$this->parseOutput($output, $path);
383 762
384
		if ($mode !== FileInfo::MODE_NORMAL) {
385
			// then set the modes we want
386
			$cmd = 'setmode ' . $path . ' ' . $modeString;
387
			$output = $this->execute($cmd);
388
			return $this->parseOutput($output, $path);
389
		} else {
390
			return true;
391
		}
392
	}
393
394
	/**
395
	 * @param string $path
396
	 * @return INotifyHandler
397
	 * @throws ConnectionException
398
	 * @throws DependencyException
399
	 */
400 762
	public function notify($path) {
401 762
		if (!$this->system->getStdBufPath()) { //stdbuf is required to disable smbclient's output buffering
402 762
			throw new DependencyException('stdbuf is required for usage of the notify command');
403
		}
404 60
		$connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process
405
		$command = 'notify ' . $this->escapePath($path);
406
		$connection->write($command . PHP_EOL);
407
		return new NotifyHandler($connection, $path);
408
	}
409
410
	/**
411
	 * @param string $command
412
	 * @return array
413
	 */
414
	protected function execute($command) {
415
		$this->connect();
416
		$this->connection->write($command . PHP_EOL);
417
		return $this->connection->read();
418
	}
419
420
	/**
421 765
	 * check output for errors
422 765
	 *
423 765
	 * @param string[] $lines
424 3
	 * @param string $path
425
	 *
426 765
	 * @throws NotFoundException
427 765
	 * @throws \Icewind\SMB\Exception\AlreadyExistsException
428 765
	 * @throws \Icewind\SMB\Exception\AccessDeniedException
429 765
	 * @throws \Icewind\SMB\Exception\NotEmptyException
430
	 * @throws \Icewind\SMB\Exception\InvalidTypeException
431
	 * @throws \Icewind\SMB\Exception\Exception
432
	 * @return bool
433
	 */
434
	protected function parseOutput($lines, $path = '') {
435
		if (count($lines) === 0) {
436 399
			return true;
437 399
		} else {
438 399
			$this->parser->checkForError($lines, $path);
439
			return false;
440
		}
441 768
	}
442 768
443 768
	/**
444
	 * @param string $string
445
	 * @return string
446
	 */
447
	protected function escape($string) {
448
		return escapeshellarg($string);
449
	}
450
451
	/**
452
	 * @param string $path
453
	 * @return string
454
	 */
455
	protected function escapePath($path) {
456
		$this->verifyPath($path);
457
		if ($path === '/') {
458
			$path = '';
459
		}
460
		$path = str_replace('/', '\\', $path);
461
		$path = str_replace('"', '^"', $path);
462
		$path = ltrim($path, '\\');
463
		return '"' . $path . '"';
464
	}
465
466
	/**
467
	 * @param string $path
468
	 * @return string
469
	 */
470
	protected function escapeLocalPath($path) {
471
		$path = str_replace('"', '\"', $path);
472
		return '"' . $path . '"';
473
	}
474
475
	public function __destruct() {
476
		unset($this->connection);
477
	}
478
}
479