Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 166 additions & 59 deletions apps/files_external/lib/Lib/Storage/SMB.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Arthur Schiwon <blizzz@owncloud.com>
* @author Jesús Macias <[email protected]>
* @author Jörn Friedrich Dreyer <[email protected]>
* @author Michael Gapczynski <[email protected]>
Expand All @@ -11,7 +11,7 @@
* @author Thomas Müller <[email protected]>
* @author Vincent Petry <[email protected]>
*
* @copyright Copyright (c) 2016, ownCloud GmbH.
* @copyright Copyright (c) 2016, ownCloud, Inc.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be gmbh - but we will fix this with the license script

* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
Expand All @@ -30,20 +30,24 @@

namespace OCA\Files_External\Lib\Storage;

use Icewind\SMB\Exception\AlreadyExistsException;
use Icewind\SMB\Exception\ConnectException;
use Icewind\SMB\Exception\Exception;
use Icewind\SMB\Exception\ForbiddenException;
use Icewind\SMB\Exception\NotFoundException;
use Icewind\SMB\FileInfo;
use Icewind\SMB\NativeServer;
use Icewind\SMB\Server;
use Icewind\SMB\Share;
use Icewind\Streams\CallbackWrapper;
use Icewind\Streams\IteratorDirectory;
use OC\Cache\CappedMemoryCache;
use OC\Files\Filesystem;
use OC\Files\Storage\Common;
use OCP\Files\StorageNotAvailableException;
use OCP\Util;

class SMB extends \OC\Files\Storage\Common {
class SMB extends Common {
/**
* @var \Icewind\SMB\Server
*/
Expand Down Expand Up @@ -74,8 +78,10 @@ public function __construct($params) {

if (isset($params['host']) && isset($params['user']) && isset($params['password']) && isset($params['share'])) {
if (Server::NativeAvailable()) {
$this->log('using native libsmbclient');
$this->server = new NativeServer($params['host'], $params['user'], $params['password']);
} else {
$this->log('falling back to smbclient');
$this->server = new Server($params['host'], $params['user'], $params['password']);
}
$this->share = $this->server->getShare(trim($params['share'], '/'));
Expand Down Expand Up @@ -120,24 +126,66 @@ protected function buildPath($path) {
* @param string $path
* @return \Icewind\SMB\IFileInfo
* @throws StorageNotAvailableException
* @throws ForbiddenException
* @throws NotFoundException
*/
protected function getFileInfo($path) {
$this->log('enter: '.__FUNCTION__."($path)");
try {
$path = $this->buildPath($path);
if (!isset($this->statCache[$path])) {
$this->log("stat fetching '{$this->root}$path'");
$this->statCache[$path] = $this->share->stat($path);
try {
$this->log("stat fetching '$path'");
try {
$this->statCache[$path] = $this->share->stat($path);
} catch (NotFoundException $e) {
if ($this->share instanceof Share) {
// smbclient may have problems with the allinfo cmd
$this->log("stat for '$path' failed, trying to read parent dir");
$infos = $this->share->dir(dirname($path));
foreach ($infos as $fileInfo) {
if ($fileInfo->getName() === basename($path)) {
$this->statCache[$path] = $fileInfo;
break;
}
}
if (empty($this->statCache[$path])) {
$this->leave(__FUNCTION__, $e);
throw $e;
}
} else {
// trust the results of libsmb
$this->leave(__FUNCTION__, $e);
throw $e;
}
}
if ($this->isRootDir($path) && $this->statCache[$path]->isHidden()) {
$this->log("unhiding stat for '$path'");
// make root never hidden, may happen when accessing a shared drive (mode is 22, archived and readonly - neither is true ... whatever)
if ($this->statCache[$path]->isReadOnly()) {
$mode = FileInfo::MODE_DIRECTORY & FileInfo::MODE_READONLY;
} else {
$mode = FileInfo::MODE_DIRECTORY;
}
$this->statCache[$path] = new FileInfo($path, '', 0, $this->statCache[$path]->getMTime(), $mode);
}
} catch (ConnectException $e) {
$ex = new StorageNotAvailableException(
$e->getMessage(), $e->getCode(), $e);
$this->leave(__FUNCTION__, $ex);
throw $ex;
} catch (ForbiddenException $e) {
if ($this->remoteIsShare() && $this->isRootDir($path)) { //mtime may not work for share root
$this->log("faking stat for forbidden '$path'");
$this->statCache[$path] = new FileInfo($path, '', 0, $this->shareMTime(), FileInfo::MODE_DIRECTORY);
} else {
$this->leave(__FUNCTION__, $e);
throw $e;
}
}
} else {
$this->log("stat cache hit for '$path'");
}
$result = $this->statCache[$path];
} catch (ConnectException $e) {
$ex = new StorageNotAvailableException(
$e->getMessage(), $e->getCode(), $e);
$this->leave(__FUNCTION__, $ex);
throw $ex;
}
return $this->leave(__FUNCTION__, $result);
}

Expand All @@ -150,9 +198,18 @@ protected function getFolderContents($path) {
$this->log('enter: '.__FUNCTION__."($path)");
try {
$path = $this->buildPath($path);
$result = $this->share->dir($path);
foreach ($result as $file) {
$this->statCache[$path . '/' . $file->getName()] = $file;
$result = [];
$children = $this->share->dir($path);
foreach ($children as $fileInfo) {
// check if the file is readable before adding it to the list
// can't use "isReadable" function here, use smb internals instead
if ($fileInfo->isHidden()) {
$this->log("{$fileInfo->getName()} isn't readable, skipping", Util::DEBUG);
} else {
$result[] = $fileInfo;
//remember entry so we can answer file_exists and filetype without a full stat
$this->statCache[$path . '/' . $fileInfo->getName()] = $fileInfo;
}
}
} catch (ConnectException $e) {
$ex = new StorageNotAvailableException(
Expand All @@ -168,12 +225,51 @@ protected function getFolderContents($path) {
* @return array
*/
protected function formatInfo($info) {
return array(
$result = [
'size' => $info->getSize(),
'mtime' => $info->getMTime()
);
'mtime' => $info->getMTime(),
];
if ($info->isDirectory()) {
$result['type'] = 'dir';
} else {
$result['type'] = 'file';
}
return $result;
}

/**
* Rename the files
*
* @param string $source the old name of the path
* @param string $target the new name of the path
* @return bool true if the rename is successful, false otherwise
*/
public function rename($source, $target) {
$this->log("enter: rename('$source', '$target')", Util::DEBUG);
try {
$result = $this->share->rename($this->root . $source, $this->root . $target);
$this->removeFromCache($this->root . $source);
$this->removeFromCache($this->root . $target);
} catch (AlreadyExistsException $e) {
$this->unlink($target);
$result = $this->share->rename($this->root . $source, $this->root . $target);
$this->removeFromCache($this->root . $source);
$this->removeFromCache($this->root . $target);
$this->swallow(__FUNCTION__, $e);
} catch (\Exception $e) {
$this->swallow(__FUNCTION__, $e);
$result = false;
}
return $this->leave(__FUNCTION__, $result);
}

private function removeFromCache($path) {
$path = trim($path, '/');
// TODO The CappedCache does not really clear by prefix. It just clears all.
//$this->dirCache->clear($path);
$this->statCache->clear($path);
//$this->xattrCache->clear($path);
}
/**
* @param string $path
* @return array
Expand All @@ -184,6 +280,45 @@ public function stat($path) {
return $this->leave(__FUNCTION__, $result);
}

/**
* get the best guess for the modification time of the share
* NOTE: modification times do not bubble up the directory tree, basically
* we are just guessing a time
*
* @return int the calculated mtime for the folder
*/
private function shareMTime() {
$this->log('enter: '.__FUNCTION__, Util::DEBUG);
$files = $this->share->dir($this->root);
$result = 0;
foreach ($files as $fileInfo) {
if ($fileInfo->getMTime() > $result) {
$result = $fileInfo->getMTime();
}
}
return $this->leave(__FUNCTION__, $result);
}
/**
* Check if the path is our root dir (not the smb one)
*
* @param string $path the path
* @return bool true if it's root, false if not
*/
private function isRootDir($path) {
$this->log('enter: '.__FUNCTION__."($path)", Util::DEBUG);
$result = $path === '' || $path === '/' || $path === '.';
return $this->leave(__FUNCTION__, $result);
}
/**
* Check if our root points to a smb share
*
* @return bool true if our root points to a share false otherwise
*/
private function remoteIsShare() {
$this->log('enter: '.__FUNCTION__, Util::DEBUG);
$result = $this->share->getName() && (!$this->root || $this->root === '/');
return $this->leave(__FUNCTION__, $result);
}
/**
* @param string $path
* @return bool
Expand Down Expand Up @@ -214,33 +349,6 @@ public function unlink($path) {
return $this->leave(__FUNCTION__, $result);
}

/**
* @param string $path1 the old name
* @param string $path2 the new name
* @return bool
* @throws StorageNotAvailableException
*/
public function rename($path1, $path2) {
$this->log('enter: '.__FUNCTION__."($path1, $path2)");
$result = false;
try {
$this->remove($path2);
$path1 = $this->buildPath($path1);
$path2 = $this->buildPath($path2);
$result = $this->share->rename($path1, $path2);
} catch (NotFoundException $e) {
$this->swallow(__FUNCTION__, $e);
} catch (ForbiddenException $e) {
$this->swallow(__FUNCTION__, $e);
} catch (ConnectException $e) {
$ex = new StorageNotAvailableException(
$e->getMessage(), $e->getCode(), $e);
$this->leave(__FUNCTION__, $ex);
throw $ex;
}
return $this->leave(__FUNCTION__, $result);
}

/**
* check if a file or folder has been updated since $time
*
Expand All @@ -250,14 +358,8 @@ public function rename($path1, $path2) {
*/
public function hasUpdated($path, $time) {
$this->log('enter: '.__FUNCTION__."($path, $time)");
if (!$path and $this->root == '/') {
// mtime doesn't work for shares, but giving the nature of the backend,
// doing a full update is still just fast enough
$result = true;
} else {
$actualTime = $this->filemtime($path);
$result = $actualTime > $time;
}
$actualTime = $this->filemtime($path);
$result = $actualTime > $time;
return $this->leave(__FUNCTION__, $result);
}

Expand Down Expand Up @@ -311,7 +413,7 @@ public function fopen($path, $mode) {
if (!$this->isCreatable(dirname($path))) {
break;
}
$tmpFile = \OCP\Files::tmpFile($ext);
$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
}
$source = fopen($tmpFile, $mode);
$share = $this->share;
Expand All @@ -338,7 +440,7 @@ public function rmdir($path) {
$this->log('enter: '.__FUNCTION__."($path)");
$result = false;
try {
$this->statCache = array();
$this->removeFromCache($path);
$content = $this->share->dir($this->buildPath($path));
foreach ($content as $file) {
if ($file->isDirectory()) {
Expand Down Expand Up @@ -417,8 +519,7 @@ public function mkdir($path) {
$result = false;
$path = $this->buildPath($path);
try {
$this->share->mkdir($path);
$result = true;
$result = $this->share->mkdir($path);
} catch (ConnectException $e) {
$ex = new StorageNotAvailableException(
$e->getMessage(), $e->getCode(), $e);
Expand Down Expand Up @@ -519,7 +620,6 @@ public function test() {
return $this->leave(__FUNCTION__, $result);
}


/**
* @param string $message
* @param int $level
Expand Down Expand Up @@ -557,7 +657,7 @@ private function leave($function, $result) {
.' message: '.$result->getMessage()
.' trace: '.$result->getTraceAsString(), Util::DEBUG);
} else {
Util::writeLog('wnd', "leave: $function, return ".json_encode($result), Util::DEBUG);
Util::writeLog('wnd', "leave: $function, return ".json_encode($result, true), Util::DEBUG);
}
return $result;
}
Expand All @@ -570,4 +670,11 @@ private function swallow($function, \Exception $exception) {
.' trace: '.$exception->getTraceAsString(), Util::DEBUG);
}
}

/**
* immediately close / free connection
*/
public function __destruct() {
unset($this->share);
}
}
4 changes: 3 additions & 1 deletion apps/files_external/tests/Storage/SmbTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ protected function setUp() {
}
$config['root'] .= $id; //make sure we have an new empty folder to work in
$this->instance = new SMB($config);
$this->instance->mkdir('/');
$this->assertTrue($this->instance->mkdir('/'));
}

protected function tearDown() {
if ($this->instance) {
$this->instance->rmdir('');
// force disconnect of the client
unset($this->instance);
}

parent::tearDown();
Expand Down