mirror of
https://codeberg.org/icewind/SMB.git
synced 2026-06-03 17:24:07 +02:00
459 lines
12 KiB
PHP
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);
|
|
}
|
|
}
|