Completed
Push — notify ( cf23de )
by Robin
02:11
created

Share::notify()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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