smb/src/Share.php
2014-08-27 10:12:33 +02:00

459 lines
12 KiB
PHP

<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\SMB;
use Icewind\SMB\Exception\AccessDeniedException;
use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\ConnectionException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\InvalidTypeException;
use Icewind\SMB\Exception\NotEmptyException;
use Icewind\SMB\Exception\NotFoundException;
use Icewind\Streams\CallbackWrapper;
class Share implements IShare {
/**
* @var Server $server
*/
private $server;
/**
* @var string $name
*/
private $name;
/**
* @var Connection $connection
*/
public $connection;
private $serverTimezone;
/**
* @param Server $server
* @param string $name
*/
public function __construct($server, $name) {
$this->server = $server;
$this->name = $name;
}
/**
* @throws \Icewind\SMB\Exception\ConnectionError
* @throws \Icewind\SMB\Exception\AuthenticationException
* @throws \Icewind\SMB\Exception\InvalidHostException
*/
protected function connect() {
if ($this->connection and $this->connection->isValid()) {
return;
}
$command = sprintf('%s --authentication-file=/proc/self/fd/3 //%s/%s',
Server::CLIENT,
$this->server->getHost(),
$this->name
);
$this->connection = new Connection($command);
$this->connection->writeAuthentication($this->server->getUser(), $this->server->getPassword());
if (!$this->connection->isValid()) {
throw new ConnectionException();
}
}
/**
* Get the name of the share
*
* @return string
*/
public function getName() {
return $this->name;
}
protected function simpleCommand($command, $path) {
$path = $this->escapePath($path);
$cmd = $command . ' ' . $path;
$output = $this->execute($cmd);
return $this->parseOutput($output);
}
private function getServerTimeZone() {
if (!$this->serverTimezone) {
$this->serverTimezone = $this->server->getTimeZone();
}
return $this->serverTimezone;
}
/**
* List the content of a remote folder
*
* @param $path
* @return \Icewind\SMB\IFileInfo[]
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function dir($path) {
$escapedPath = $this->escapePath($path);
$output = $this->execute('cd ' . $escapedPath);
//check output for errors
$this->parseOutput($output);
$output = $this->execute('dir');
$this->execute('cd /');
//last line is used space
array_pop($output);
$regex = '/^\s*(.*?)\s\s\s\s+(?:([NDHARS]*)\s+)?([0-9]+)\s+(.*)$/';
//2 spaces, filename, optional type, size, date
$content = array();
foreach ($output as $line) {
if (preg_match($regex, $line, $matches)) {
list(, $name, $mode, $size, $time) = $matches;
if ($name !== '.' and $name !== '..') {
$mode = $this->parseMode($mode);
$time = strtotime($time . ' ' . $this->getServerTimeZone());
$content[] = new FileInfo($path . '/' . $name, $name, $size, $time, $mode);
}
}
}
return $content;
}
/**
* @param string $path
* @return \Icewind\SMB\IFileInfo[]
*/
public function stat($path) {
$escapedPath = $this->escapePath($path);
$output = $this->execute('allinfo ' . $escapedPath);
if (count($output) < 3) {
$this->parseOutput($output);
}
$mtime = 0;
$mode = 0;
$size = 0;
foreach ($output as $line) {
list($name, $value) = explode(':', $line, 2);
$value = trim($value);
if ($name === 'write_time') {
$mtime = $value;
} else if ($name === 'attributes') {
$mode = $this->parseMode($value);
} else if ($name === 'stream') {
list(, $size,) = explode(' ', $value);
$size = intval($size);
}
}
return new FileInfo($path, basename($path), $size, $mtime, $mode);
}
/**
* Create a folder on the share
*
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\AlreadyExistsException
*/
public function mkdir($path) {
return $this->simpleCommand('mkdir', $path);
}
/**
* Remove a folder on the share
*
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function rmdir($path) {
return $this->simpleCommand('rmdir', $path);
}
/**
* Delete a file on the share
*
* @param string $path
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function del($path) {
//del return a file not found error when trying to delete a folder
//we catch it so we can check if $path doesn't exist or is of invalid type
try {
return $this->simpleCommand('del', $path);
} catch (NotFoundException $e) {
//no need to do anything with the result, we just check if this throws the not found error
try {
$this->simpleCommand('ls', $path);
} catch (NotFoundException $e2) {
throw $e;
} catch (\Exception $e2) {
throw new InvalidTypeException();
}
throw $e;
}
}
/**
* Rename a remote file
*
* @param string $from
* @param string $to
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\AlreadyExistsException
*/
public function rename($from, $to) {
$path1 = $this->escapePath($from);
$path2 = $this->escapePath($to);
$cmd = 'rename ' . $path1 . ' ' . $path2;
$output = $this->execute($cmd);
return $this->parseOutput($output);
}
/**
* Upload a local file
*
* @param string $source local file
* @param string $target remove file
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function put($source, $target) {
$path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping
$path2 = $this->escapePath($target);
$output = $this->execute('put ' . $path1 . ' ' . $path2);
return $this->parseOutput($output);
}
/**
* Download a remote file
*
* @param string $source remove file
* @param string $target local file
* @return bool
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function get($source, $target) {
$path1 = $this->escapePath($source);
$path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping
$output = $this->execute('get ' . $path1 . ' ' . $path2);
return $this->parseOutput($output);
}
/**
* Open a readable stream to a remote file
*
* @param string $source
* @return resource a read only stream with the contents of the remote file
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function read($source) {
$source = $this->escapePath($source);
// close the single quote, open a double quote where we put the single quote...
$source = str_replace('\'', '\'"\'"\'', $source);
// since returned stream is closed by the caller we need to create a new instance
// since we can't re-use the same file descriptor over multiple calls
$command = sprintf('%s --authentication-file=/proc/self/fd/3 //%s/%s -c \'get %s /proc/self/fd/5\'',
Server::CLIENT,
$this->server->getHost(),
$this->name,
$source
);
$connection = new Connection($command);
$connection->writeAuthentication($this->server->getUser(), $this->server->getPassword());
$fh = $connection->getFileOutputStream();
stream_context_set_option($fh, 'file', 'connection', $connection);
return $fh;
}
/**
* Open a writable stream to a remote file
*
* @param string $target
* @return resource a write only stream to upload a remote file
*
* @throws \Icewind\SMB\Exception\NotFoundException
* @throws \Icewind\SMB\Exception\InvalidTypeException
*/
public function write($target) {
$target = $this->escapePath($target);
// close the single quote, open a double quote where we put the single quote...
$target = str_replace('\'', '\'"\'"\'', $target);
// since returned stream is closed by the caller we need to create a new instance
// since we can't re-use the same file descriptor over multiple calls
$command = sprintf('%s --authentication-file=/proc/self/fd/3 //%s/%s -c \'put /proc/self/fd/4 %s\'',
Server::CLIENT,
$this->server->getHost(),
$this->name,
$target
);
$connection = new RawConnection($command);
$connection->writeAuthentication($this->server->getUser(), $this->server->getPassword());
$fh = $connection->getFileInputStream();
// use a close callback to ensure the upload is finished before continuing
// this also serves as a way to keep the connection in scope
return CallbackWrapper::wrap($fh, null, null, function () use ($connection) {
$connection->close(false); // dont terminate, give the upload some time
});
}
/**
* @param string $path
* @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL
* @return mixed
*/
public function setMode($path, $mode) {
$modeString = '';
if ($mode & FileInfo::MODE_READONLY) {
$modeString .= 'r';
}
if ($mode & FileInfo::MODE_HIDDEN) {
$modeString .= 'h';
}
if ($mode & FileInfo::MODE_ARCHIVE) {
$modeString .= 'a';
}
if ($mode & FileInfo::MODE_SYSTEM) {
$modeString .= 's';
}
$path = $this->escapePath($path);
// first reset the mode to normal
$cmd = 'setmode ' . $path . ' -rsha';
$output = $this->execute($cmd);
$this->parseOutput($output);
// then set the modes we want
$cmd = 'setmode ' . $path . ' ' . $modeString;
$output = $this->execute($cmd);
return $this->parseOutput($output);
}
/**
* @param string $mode
* @return string
*/
protected function parseMode($mode) {
$result = 0;
$modeStrings = array(
'R' => FileInfo::MODE_READONLY,
'H' => FileInfo::MODE_HIDDEN,
'S' => FileInfo::MODE_SYSTEM,
'D' => FileInfo::MODE_DIRECTORY,
'A' => FileInfo::MODE_ARCHIVE
);
foreach ($modeStrings as $char => $val) {
if (strpos($mode, $char) !== false) {
$result |= $val;
}
}
return $result;
}
/**
* @param string $command
* @return array
*/
protected function execute($command) {
$this->connect();
$this->connection->write($command . PHP_EOL);
$output = $this->connection->read();
return $output;
}
/**
* check output for errors
*
* @param $lines
*
* @throws NotFoundException
* @throws \Icewind\SMB\Exception\AlreadyExistsException
* @throws \Icewind\SMB\Exception\AccessDeniedException
* @throws \Icewind\SMB\Exception\NotEmptyException
* @throws \Icewind\SMB\Exception\InvalidTypeException
* @throws \Icewind\SMB\Exception\Exception
* @return bool
*/
protected function parseOutput($lines) {
if (count($lines) === 0) {
return true;
} else {
if (strpos($lines[0], 'does not exist')) {
throw new NotFoundException();
}
$parts = explode(' ', $lines[0]);
$error = false;
foreach ($parts as $part) {
if (substr($part, 0, 9) === 'NT_STATUS') {
$error = $part;
}
}
switch ($error) {
case ErrorCodes::PathNotFound:
case ErrorCodes::ObjectNotFound:
case ErrorCodes::NoSuchFile:
throw new NotFoundException();
case ErrorCodes::NameCollision:
throw new AlreadyExistsException();
case ErrorCodes::AccessDenied:
throw new AccessDeniedException();
case ErrorCodes::DirectoryNotEmpty:
throw new NotEmptyException();
case ErrorCodes::FileIsADirectory:
case ErrorCodes::NotADirectory:
throw new InvalidTypeException();
default:
throw new Exception();
}
}
}
/**
* @param string $string
* @return string
*/
protected function escape($string) {
return escapeshellarg($string);
}
/**
* @param string $path
* @return string
*/
protected function escapePath($path) {
$path = str_replace('/', '\\', $path);
$path = str_replace('"', '^"', $path);
return '"' . $path . '"';
}
/**
* @param string $path
* @return string
*/
protected function escapeLocalPath($path) {
$path = str_replace('"', '\"', $path);
return '"' . $path . '"';
}
public function __destruct() {
unset($this->connection);
}
}