Completed
Push — notify ( 727fed...e1b943 )
by Robin
02:13
created

Share::getConnection()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
rs 9.4285
cc 3
eloc 12
nc 4
nop 0
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) {
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...
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) {
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...
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) {
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...
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