Introduce FileInfo objects for the result of dir and add readable and hidden attributes to files

This commit is contained in:
Robin Appelman 2014-07-31 16:05:20 +02:00
commit 1c11289d36
7 changed files with 452 additions and 53 deletions

111
src/FileInfo.php Normal file
View file

@ -0,0 +1,111 @@
<?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;
class FileInfo implements IFileInfo {
/*
* Mappings of the DOS mode bits, as returned by smbc_getxattr() when the
* attribute name "system.dos_attr.mode" (or "system.dos_attr.*" or
* "system.*") is specified.
*/
const MODE_READONLY = 0x01;
const MODE_HIDDEN = 0x02;
const MODE_SYSTEM = 0x04;
const MODE_VOLUME_ID = 0x08;
const MODE_DIRECTORY = 0x10;
const MODE_ARCHIVE = 0x20;
/**
* @var string
*/
protected $path;
/**
* @var string
*/
protected $name;
/**
* @var int
*/
protected $size;
/**
* @var int
*/
protected $time;
/**
* @var int
*/
protected $mode;
/**
* @param string $path
* @param string $name
* @param int $size
* @param int $time
* @param int $mode
*/
public function __construct($path, $name, $size, $time, $mode) {
$this->path = $path;
$this->name = $name;
$this->size = $size;
$this->time = $time;
$this->mode = $mode;
}
/**
* @return string
*/
public function getPath() {
return $this->path;
}
/**
* @return string
*/
public function getName() {
return $this->name;
}
/**
* @return int
*/
public function getSize() {
return $this->size;
}
/**
* @return int
*/
public function getMTime() {
return $this->time;
}
/**
* @return bool
*/
public function isDirectory() {
return (bool)($this->mode & self::MODE_DIRECTORY);
}
/**
* @return bool
*/
public function isReadOnly() {
return (bool)($this->mode & self::MODE_READONLY);
}
/**
* @return bool
*/
public function isHidden() {
return (bool)($this->mode & self::MODE_HIDDEN);
}
}

45
src/IFileInfo.php Normal file
View file

@ -0,0 +1,45 @@
<?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;
interface IFileInfo {
/**
* @return string
*/
public function getPath();
/**
* @return string
*/
public function getName();
/**
* @return int
*/
public function getSize();
/**
* @return int
*/
public function getMTime();
/**
* @return bool
*/
public function isDirectory();
/**
* @return bool
*/
public function isReadOnly();
/**
* @return bool
*/
public function isHidden();
}

View file

@ -98,7 +98,7 @@ interface IShare {
* ] * ]
* *
* @param $path * @param $path
* @return array[] * @return \Icewind\SMB\IFileInfo[]
* *
* @throws \Icewind\SMB\NotFoundException * @throws \Icewind\SMB\NotFoundException
* @throws \Icewind\SMB\InvalidTypeException * @throws \Icewind\SMB\InvalidTypeException

122
src/NativeFileInfo.php Normal file
View file

@ -0,0 +1,122 @@
<?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;
class NativeFileInfo implements IFileInfo {
const MODE_FILE = 0100000;
/**
* @var string
*/
protected $path;
/**
* @var string
*/
protected $name;
/**
* @var \Icewind\SMB\NativeShare
*/
protected $share;
/**
* @var array
*/
protected $statCache;
/**
* @var int
*/
protected $modeCache;
/**
* @param \Icewind\SMB\NativeShare $share
* @param string $path
* @param string $name
*/
public function __construct($share, $path, $name) {
$this->share = $share;
$this->path = $path;
$this->name = $name;
}
/**
* @return string
*/
public function getPath() {
return $this->path;
}
/**
* @return string
*/
public function getName() {
return $this->name;
}
/**
* @return array
*/
protected function stat() {
if (!$this->statCache) {
$this->statCache = $this->share->stat($this->getPath());
}
return $this->statCache;
}
/**
* @return int
*/
public function getSize() {
$stat = $this->stat();
return $stat['size'];
}
/**
* @return int
*/
public function getMTime() {
$stat = $this->stat();
return $stat['mtime'];
}
/**
* @return bool
*/
public function isDirectory() {
$stat = $this->stat();
return !($stat['mode'] & self::MODE_FILE);
}
/**
* @return int
*/
protected function getMode() {
if (!$this->modeCache) {
$this->modeCache = $this->share->getAttribute($this->path, 'system.dos_attr.mode');
}
return $this->modeCache;
}
/**
* @return bool
*/
public function isReadOnly() {
$mode = $this->getMode();
return (bool)($mode & FileInfo::MODE_READONLY);
}
/**
* @return bool
*/
public function isHidden() {
$mode = $this->getMode();
return (bool)($mode & FileInfo::MODE_HIDDEN);
}
}

View file

@ -101,7 +101,8 @@ class NativeShare implements IShare {
} else if (strpos($errorString, 'unknown error (110)') or } else if (strpos($errorString, 'unknown error (110)') or
strpos($errorString, 'unknown error (111)') or strpos($errorString, 'unknown error (111)') or
strpos($errorString, 'unknown error (112)') or strpos($errorString, 'unknown error (112)') or
strpos($errorString, 'unknown error (113)')) { strpos($errorString, 'unknown error (113)')
) {
// errors for connection timeout, connection refused, host is down and // errors for connection timeout, connection refused, host is down and
// no route to host, respectively // no route to host, respectively
throw new ConnectionError($errorString); throw new ConnectionError($errorString);
@ -138,12 +139,7 @@ class NativeShare implements IShare {
while ($file = smbclient_readdir($this->state, $dh)) { while ($file = smbclient_readdir($this->state, $dh)) {
$name = $file['name']; $name = $file['name'];
if ($name !== '.' and $name !== '..') { if ($name !== '.' and $name !== '..') {
$stat = $this->stat($path . '/' . $name); $files [] = new NativeFileInfo($this, $path . '/' . $name, $name);
$files[$name] = array(
'type' => ($file['type'] === 'directory') ? 'dir' : 'file',
'size' => $stat['size'],
'time' => $stat['mtime']
);
} }
} }
smbclient_closedir($this->state, $dh); smbclient_closedir($this->state, $dh);
@ -151,7 +147,7 @@ class NativeShare implements IShare {
return $files; return $files;
} }
protected function stat($path) { public function stat($path) {
$this->connect(); $this->connect();
self::registerErrorHandler(); self::registerErrorHandler();
$stat = smbclient_stat($this->state, $this->buildUrl($path)); $stat = smbclient_stat($this->state, $this->buildUrl($path));
@ -325,4 +321,37 @@ class NativeShare implements IShare {
$handle = $this->create($source); $handle = $this->create($source);
return NativeStream::wrap($this->state, $handle, 'w'); return NativeStream::wrap($this->state, $handle, 'w');
} }
/**
* List the available extended attributes for the path (returns a fixed list)
*
* @param string $path
* @return array list the available attributes for the path
*/
public function listAttributes($path) {
$this->connect();
self::registerErrorHandler();
$result = smbclient_listxattr($this->state, $this->buildUrl($path));
self::restoreErrorHandler();
return $result;
}
/**
* Get extended attributes for the path
*
* @param string $path
* @param string $attribute attribute to get the info
* @return string the attribute value
*/
public function getAttribute($path, $attribute) {
$this->connect();
self::registerErrorHandler();
$result = smbclient_getxattr($this->state, $this->buildUrl($path), $attribute);
self::restoreErrorHandler();
// parse hex string
if ($attribute === 'system.dos_attr.mode') {
$result = hexdec(substr($result, 2));
}
return $result;
}
} }

View file

@ -80,25 +80,15 @@ class Share implements IShare {
/** /**
* List the content of a remote folder * List the content of a remote folder
* *
* Returns a nested array in the format of
* [
* $name => [
* 'size' => $size,
* 'type' => $type,
* 'time' => $mtime
* ],
* ...
* ]
*
* @param $path * @param $path
* @return array[] * @return \Icewind\SMB\IFileInfo[]
* *
* @throws \Icewind\SMB\NotFoundException * @throws \Icewind\SMB\NotFoundException
* @throws \Icewind\SMB\InvalidTypeException * @throws \Icewind\SMB\InvalidTypeException
*/ */
public function dir($path) { public function dir($path) {
$path = $this->escapePath($path); $escapedPath = $this->escapePath($path);
$output = $this->execute('cd ' . $path); $output = $this->execute('cd ' . $escapedPath);
//check output for errors //check output for errors
$this->parseOutput($output); $this->parseOutput($output);
$output = $this->execute('dir'); $output = $this->execute('dir');
@ -111,13 +101,11 @@ class Share implements IShare {
$content = array(); $content = array();
foreach ($output as $line) { foreach ($output as $line) {
if (preg_match($regex, $line, $matches)) { if (preg_match($regex, $line, $matches)) {
list(, $name, $type, $size, $time) = $matches; list(, $name, $mode, $size, $time) = $matches;
if ($name !== '.' and $name !== '..') { if ($name !== '.' and $name !== '..') {
$content[$name] = array( $mode = $this->parseMode($mode);
'size' => intval(trim($size)), $time = strtotime($time . ' ' . $this->getServerTimeZone());
'type' => (strpos($type, 'D') !== false) ? 'dir' : 'file', $content[] = new FileInfo($path . '/' . $name, $name, $size, $time, $mode);
'time' => strtotime($time . ' ' . $this->getServerTimeZone())
);
} }
} }
} }
@ -250,7 +238,6 @@ class Share implements IShare {
$connection = new Connection($command); $connection = new Connection($command);
$connection->writeAuthentication($this->server->getUser(), $this->server->getPassword()); $connection->writeAuthentication($this->server->getUser(), $this->server->getPassword());
$fh = $connection->getFileOutputStream(); $fh = $connection->getFileOutputStream();
//save the connection as context of the stream to prevent it going out of scope and cleaning up the resource
stream_context_set_option($fh, 'file', 'connection', $connection); stream_context_set_option($fh, 'file', 'connection', $connection);
return $fh; return $fh;
} }
@ -284,6 +271,82 @@ class Share implements IShare {
}); });
} }
/**
* @param string $path
* @return array
*/
protected function getAttributes($path) {
$path = $this->escapePath($path);
$output = $this->execute('allinfo ' . $path);
$attributes = array();
foreach ($output as $line) {
list($name, $value) = explode($line, ':', 2);
$value = trim($value);
switch ($name) {
case 'create_time':
$attributes['system.dos_attr.c_time'] = strtotime($value . ' ' . $this->getServerTimeZone());
break;
case 'access_time':
$attributes['system.dos_attr.a_time'] = strtotime($value . ' ' . $this->getServerTimeZone());
break;
case 'change_time':
$attributes['system.dos_attr.m_time'] = strtotime($value . ' ' . $this->getServerTimeZone());
break;
case 'attributes':
$attributes['system.dos_attr.mode'] = $this->parseMode($value);
break;
}
}
return $attributes;
}
/**
* @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;
}
/**
* List the available extended attributes for the path (returns a fixed list)
*
* @param string $path
* @return array list the available attributes for the path
*/
public function listAttributes($path) {
return array_keys($this->getAttributes($path));
}
/**
* Get extended attributes for the path
*
* @param string $path
* @param string $attribute attribute to get the info
* @return string the attribute value
*/
public function getAttribute($path, $attribute) {
$attributes = $this->getAttributes($path);
if (isset($attributes[$attribute])) {
return $attributes[$attribute];
} else {
return null;
}
}
/** /**
* @param string $command * @param string $command
* @return array * @return array

View file

@ -63,11 +63,11 @@ abstract class AbstractShare extends \PHPUnit_Framework_TestCase {
public function cleanDir($dir) { public function cleanDir($dir) {
$content = $this->share->dir($dir); $content = $this->share->dir($dir);
foreach ($content as $name => $metadata) { foreach ($content as $metadata) {
if ($metadata['type'] === 'dir') { if ($metadata->isDirectory()) {
$this->cleanDir($dir . '/' . $name); $this->cleanDir($metadata->getPath());
} else { } else {
$this->share->del($dir . '/' . $name); $this->share->del($metadata->getPath());
} }
} }
$this->share->rmdir($dir); $this->share->rmdir($dir);
@ -97,17 +97,17 @@ abstract class AbstractShare extends \PHPUnit_Framework_TestCase {
$this->share->mkdir($this->root . '/foo'); $this->share->mkdir($this->root . '/foo');
$dirs = $this->share->dir($this->root); $dirs = $this->share->dir($this->root);
$this->assertEquals(1, count($dirs)); $this->assertCount(1, $dirs);
$this->assertArrayHasKey('foo', $dirs); $this->assertEquals('foo', $dirs[0]->getName());
$this->share->rename($this->root . '/foo', $this->root . '/bar'); $this->share->rename($this->root . '/foo', $this->root . '/bar');
$dirs = $this->share->dir($this->root); $dirs = $this->share->dir($this->root);
$this->assertEquals(1, count($dirs)); $this->assertEquals(1, count($dirs));
$this->assertArrayHasKey('bar', $dirs); $this->assertEquals('bar', $dirs[0]->getName());
$this->share->rmdir($this->root . '/bar'); $this->share->rmdir($this->root . '/bar');
$this->assertEquals(array(), $this->share->dir($this->root)); $this->assertCount(0, $this->share->dir($this->root));
} }
/** /**
@ -122,15 +122,15 @@ abstract class AbstractShare extends \PHPUnit_Framework_TestCase {
unlink($tmpFile1); unlink($tmpFile1);
$files = $this->share->dir($this->root); $files = $this->share->dir($this->root);
$this->assertEquals(1, count($files)); $this->assertCount(1, $files);
$this->assertArrayHasKey('lorem.txt', $files); $this->assertEquals('lorem.txt', $files[0]->getName());
$this->assertEquals($files['lorem.txt']['size'], $size); $this->assertEquals($size, $files[0]->getSize());
$this->share->rename($this->root . '/lorem.txt', $this->root . '/foo.txt'); $this->share->rename($this->root . '/lorem.txt', $this->root . '/foo.txt');
$files = $this->share->dir($this->root); $files = $this->share->dir($this->root);
$this->assertEquals(1, count($files)); $this->assertEquals(1, count($files));
$this->assertArrayHasKey('foo.txt', $files); $this->assertEquals('foo.txt', $files[0]->getName());
$tmpFile2 = tempnam('/tmp', 'smb_test_'); $tmpFile2 = tempnam('/tmp', 'smb_test_');
$this->share->get($this->root . '/foo.txt', $tmpFile2); $this->share->get($this->root . '/foo.txt', $tmpFile2);
@ -139,7 +139,7 @@ abstract class AbstractShare extends \PHPUnit_Framework_TestCase {
unlink($tmpFile2); unlink($tmpFile2);
$this->share->del($this->root . '/foo.txt'); $this->share->del($this->root . '/foo.txt');
$this->assertEquals(array(), $this->share->dir($this->root)); $this->assertCount(0, $this->share->dir($this->root));
} }
/** /**
@ -152,13 +152,13 @@ abstract class AbstractShare extends \PHPUnit_Framework_TestCase {
$this->share->mkdir($this->root . '/' . $name); $this->share->mkdir($this->root . '/' . $name);
$dir = $this->share->dir($this->root); $dir = $this->share->dir($this->root);
$this->assertArrayHasKey($name, $dir); $this->assertEquals($name, $dir[0]->getName());
$this->assertEquals('dir', $dir[$name]['type']); $this->assertTrue($dir[0]->isDirectory());
$this->assertEquals(array(), $this->share->dir($this->root . '/' . $name)); $this->assertCount(0, $this->share->dir($this->root . '/' . $name));
$this->share->put($tmpFile1, $this->root . '/' . $name . '/foo.txt'); $this->share->put($tmpFile1, $this->root . '/' . $name . '/foo.txt');
$dir = $this->share->dir($this->root . '/' . $name); $dir = $this->share->dir($this->root . '/' . $name);
$this->assertArrayHasKey('foo.txt', $dir); $this->assertEquals('foo.txt', $dir[0]->getName());
$tmpFile2 = tempnam('/tmp', 'smb_test_'); $tmpFile2 = tempnam('/tmp', 'smb_test_');
$this->share->get($this->root . '/' . $name . '/foo.txt', $tmpFile2); $this->share->get($this->root . '/' . $name . '/foo.txt', $tmpFile2);
@ -167,16 +167,17 @@ abstract class AbstractShare extends \PHPUnit_Framework_TestCase {
$this->share->rename($this->root . '/' . $name . '/foo.txt', $this->root . '/' . $name . '/bar.txt'); $this->share->rename($this->root . '/' . $name . '/foo.txt', $this->root . '/' . $name . '/bar.txt');
$dir = $this->share->dir($this->root . '/' . $name); $dir = $this->share->dir($this->root . '/' . $name);
$this->assertArrayHasKey('bar.txt', $dir); $this->assertEquals('bar.txt', $dir[0]->getName());
$this->assertEquals('file', $dir['bar.txt']['type']); $this->assertFalse($dir[0]->isDirectory());
$this->share->del($this->root . '/' . $name . '/bar.txt'); $this->share->del($this->root . '/' . $name . '/bar.txt');
$this->assertEquals(array(), $this->share->dir($this->root . '/' . $name)); $this->assertCount(0, $this->share->dir($this->root . '/' . $name));
$this->share->rmdir($this->root . '/' . $name); $this->share->rmdir($this->root . '/' . $name);
$this->assertEquals(array(), $this->share->dir($this->root)); $this->assertCount(0, $this->share->dir($this->root));
$this->share->put($tmpFile1, $this->root . '/' . $name); $this->share->put($tmpFile1, $this->root . '/' . $name);
$this->assertArrayHasKey($name, $this->share->dir($this->root)); $dir = $this->share->dir($this->root);
$this->assertEquals($name, $dir[0]->getName());
$tmpFile2 = tempnam('/tmp', 'smb_test_'); $tmpFile2 = tempnam('/tmp', 'smb_test_');
$this->share->get($this->root . '/' . $name, $tmpFile2); $this->share->get($this->root . '/' . $name, $tmpFile2);
@ -187,7 +188,8 @@ abstract class AbstractShare extends \PHPUnit_Framework_TestCase {
$tmpFile2 = tempnam('/tmp', 'smb_test_' . $name); $tmpFile2 = tempnam('/tmp', 'smb_test_' . $name);
$this->share->put($tmpFile2, $this->root . '/' . $name); $this->share->put($tmpFile2, $this->root . '/' . $name);
$this->assertArrayHasKey($name, $this->share->dir($this->root)); $dir = $this->share->dir($this->root);
$this->assertEquals($name, $dir[0]->getName());
$this->share->del($this->root . '/' . $name); $this->share->del($this->root . '/' . $name);
unlink($tmpFile2); unlink($tmpFile2);
@ -301,7 +303,7 @@ abstract class AbstractShare extends \PHPUnit_Framework_TestCase {
$now = time(); $now = time();
$this->share->put($this->getTextFile(), $this->root . '/foo.txt'); $this->share->put($this->getTextFile(), $this->root . '/foo.txt');
$dir = $this->share->dir($this->root); $dir = $this->share->dir($this->root);
$mtime = $dir['foo.txt']['time']; $mtime = $dir[0]->getMTime();
$this->assertTrue(abs($now - $mtime) <= 1, 'Modified time differs by ' . abs($now - $mtime) . ' seconds'); $this->assertTrue(abs($now - $mtime) <= 1, 'Modified time differs by ' . abs($now - $mtime) . ' seconds');
$this->share->del($this->root . '/foo.txt'); $this->share->del($this->root . '/foo.txt');
} }
@ -339,4 +341,31 @@ abstract class AbstractShare extends \PHPUnit_Framework_TestCase {
$this->share->del($this->root . '/' . $name); $this->share->del($this->root . '/' . $name);
unlink($tmpFile1); unlink($tmpFile1);
} }
public function testDir() {
$txtFile = $this->getTextFile();
$this->share->mkdir($this->root . '/dir');
$this->share->put($txtFile, $this->root . '/file.txt');
unlink($txtFile);
$dir = $this->share->dir($this->root);
if ($dir[0]->getName() === 'dir') {
$dirEntry = $dir[0];
} else {
$dirEntry = $dir[1];
}
$this->assertTrue($dirEntry->isDirectory());
$this->assertFalse($dirEntry->isReadOnly());
$this->assertFalse($dirEntry->isReadOnly());
if ($dir[0]->getName() === 'file.txt') {
$fileEntry = $dir[0];
} else {
$fileEntry = $dir[1];
}
$this->assertFalse($fileEntry->isDirectory());
$this->assertFalse($fileEntry->isReadOnly());
$this->assertFalse($fileEntry->isReadOnly());
}
} }