Skip to content

Instantly share code, notes, and snippets.

@jpriebe
Created March 21, 2017 14:30
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save jpriebe/293aec9ba37e1d5c0570b757aab9aa74 to your computer and use it in GitHub Desktop.
Save jpriebe/293aec9ba37e1d5c0570b757aab9aa74 to your computer and use it in GitHub Desktop.
simple PHP smbclient
<?php
/**
* Class for interacting with an SMB server using the system command "smbclient".
* Of course this assumes that you have the smbclient executable installed and
* in your path.
*
* It is not the most efficient way of interacting with an SMB server -- for instance,
* putting multiple files involves running the executable multiple times and
* establishing a connection for each file. However, if performance is not an
* issue, this is a quick-and-dirty way to move files to and from the SMB
* server from PHP.
*
* Note that this library relies heavily on the exit code from smbclient to determine
* success/failure of operations. Unfortunately, smbclient is not very good about
* using the error codes. A "put" command that cannot write will return an exit code
* of "1", but a "del" command on a non-existent file will exit with a status of 0.
*
* So this method is imperfect. Better would be to actually parse the output of
* smbclient even when the exit status is 0 to see if there are codes like
* NT_STATUS_NO_SUCH_FILE or NT_STATUS_OBJECT_NAME_NOT_FOUND
*/
class smbclient
{
// when doing "safe" puts, how many times are we willing to retry if we
// fail to send? And how long (in ms) to wait between retries
private $_max_safe_retries = 3;
private $_safe_retry_interval = 200;
public static $debug_mode = false;
public static $debug_label = 'smbclient';
private $_service;
private $_username;
private $_password;
private $_cmd;
/**
* Gets the most recently executed command string
*
* @return string
*/
public function get_last_cmd () { return $this->_cmd; }
private $_last_cmd_stdout;
/**
* Gets stndard output from the last run command; can be useful in
* case the command reports an error; smbclient writes a lot of
* diagnostics to stdout.
*
* @return array each line of stdout is one string in the array
*/
public function get_last_cmd_stdout () { return $this->_last_cmd_stdout; }
private $_last_cmd_stderr;
/**
* Gets stndard error from the last run command
*
* @return array each line of stderr is one string in the array
*/
public function get_last_cmd_stderr () { return $this->_last_cmd_stderr; }
private $_last_cmd_exit_code;
/**
* Gets the exit code of the last command run
*
* @return int
*/
public function get_last_cmd_exit_code () { return $this->_last_cmd_exit_code; }
private $_safe_retry_count = 0;
/**
* Gets the retry count of the last safe_put() operation; if it
* succeeded on the first try, retry count is 0
*
* @return int
*/
public function get_safe_retry_count () { return $this->_safe_retry_count; }
/**
* Creates an smbclient object
*
* @param string $service the UNC service name
* @param string $username the username to use when connecting
* @param string $password the password to use when connecting
*/
public function __construct ($service, $username, $password)
{
$this->_service = $service;
$this->_username = $username;
$this->_password = $password;
}
/**
* Gets a remote file
*
* @param string $remote_filename remote filename (use the local system's directory separators)
* @param string $local_filename the full path to the local filename
* @return bool true if successful, false otherwise
*/
public function get ($remote_filename, $local_filename)
{
// convert to windows-style backslashes
$remote_filename = str_replace (DIRECTORY_SEPARATOR, '\\', $remote_filename);
$cmd = "get \"$remote_filename\" \"$local_filename\"";
$retval = $this->execute ($cmd);
return $retval;
}
/**
* Puts multiple local files on the server
*
* @param array $local_files array of local filename paths
* @param string $remote_path path to remote directory (use the local system's directory separators)
* @param bool $safe use safe_put() instead of put ()
* @return bool true if successful, false otherwise
*/
public function mput ($local_files, $remote_path, $safe = false)
{
foreach ($local_files as $local_file)
{
$pi = pathinfo ($local_file);
$remote_file = $remote_path . '/' . $pi['basename'];
if ($safe)
{
if (!$this->safe_put ($local_file, $remote_file))
{
return false;
}
}
else
{
if (!$this->put ($local_file, $remote_file))
{
return false;
}
}
}
return true;
}
/**
* Puts a local file
*
* @param string $local_filename the full path to local filename
* @param string $remote_filename (use the local system's directory separators)
* @return bool true if successful, false otherwise
*/
public function put ($local_filename, $remote_filename)
{
// convert to windows-style backslashes
$remote_filename = str_replace (DIRECTORY_SEPARATOR, '\\', $remote_filename);
$cmd = "put \"$local_filename\" \"$remote_filename\"";
$retval = $this->execute ($cmd);
return $retval;
}
/**
* Safely puts a local file; it writes to a temporary file, retrieves
* that file, compares checksum to the original local file, then if
* everything checks out, renames the remote temporary file to its
* final remote filename.
*
* @param string $local_filename the full path to local filename
* @param string $remote_filename (use the local system's directory separators)
* @return bool true if successful, false otherwise
*/
public function safe_put ($local_filename, $remote_filename)
{
// I wanted to write to a temp file on the remote system, then rename the
// file, but Windows won't let you do that. So all I can do is write to
// the permanent file, then check its contents immediately. Two problems
// with this:
// - if the data transfer doesn't work, I've now wrecked the file
// - if another process writes to the file between the time I send
// the data and the time I read it, I'm going to think the data
// transfer failed.
//
// all the commented-out code was designed to use this strategy, before
// I found that it doesn't work. :-(
//$tmp_remote_filename = $remote_filename . '.' . uniqid () . '.tmp';
$tmp_local_filename = tempnam(sys_get_temp_dir(), 'safe_put');
$local_crc = crc32 (file_get_contents ($local_filename));
$success = false;
$this->_safe_retry_count = 0;
while (!$success)
{
self::log_msg ("retry count: " . $this->_safe_retry_count);
//if ($this->put ($local_filename, $tmp_remote_filename))
if ($this->put ($local_filename, $remote_filename))
{
//if ($this->get ($tmp_remote_filename, $tmp_local_filename))
if ($this->get ($remote_filename, $tmp_local_filename))
{
self::log_msg ("contents: '" . file_get_contents ($tmp_local_filename) . "'");
if (crc32 (file_get_contents ($tmp_local_filename)) == $local_crc)
{
unlink ($tmp_local_filename);
self::log_msg ("retrieved file matches CRC32");
$success = true;
return true;
/*
if ($this->rename ($tmp_remote_filename, $remote_filename))
{
$success = true;
return true;
}
else
{
array_unshift ($this->_last_cmd_stderr, "safe_put() failed to rename file");
}
*/
}
else
{
self::log_msg ("retrieved file does not match CRC32");
array_unshift ($this->_last_cmd_stderr, "safe_put() failed to validate checksum of $tmp_remote_filename");
/*
if (!$this->del ($tmp_remote_filename))
{
array_unshift ($this->_last_cmd_stderr, "safe_put() failed to validate checksum of $tmp_remote_filename and failed to delete it from remote machine: " . $this->_last_cmd_stderr);
}
*/
}
unlink ($tmp_local_filename);
}
}
if ($this->_safe_retry_count > $this->_max_safe_retries)
{
self::log_msg ("out of retries");
break;
}
$this->_safe_retry_count++;
usleep ($this->_safe_retry_interval);
}
unlink ($tmp_local_filename);
return false;
}
/**
* Renames a remote file
*
* @param string $source_filename the remote source file (use the local system's directory separators)
* @param string $dest_filename the remote destination file (use the local system's directory separators)
* @return bool true if successful, false otherwise
*/
public function rename ($source_filename, $dest_filename)
{
// convert to windows-style backslashes
$source_filename = str_replace (DIRECTORY_SEPARATOR, '\\', $source_filename);
$dest_filename = str_replace (DIRECTORY_SEPARATOR, '\\', $dest_filename);
$cmd = "rename \"$source_filename\" \"$dest_filename\"";
$retval = $this->execute ($cmd);
return $retval;
}
/**
* Deletes a remote file
*
* Note: due to limitations in smbclient, if the remote filename specifies a path,
* we can't do this in one command; instead, we need to break it into a cd and then a del.
* This is unfortunate, because if the path is specified incorrectly, and the cd fails,
* we may delete the wrong file.
*
* @param string $remote_filename (use the local system's directory separators)
* @return bool true if successful, false otherwise
*/
public function del ($remote_filename)
{
$pi = pathinfo ($remote_filename);
$remote_path = $pi['dirname'];
$basename = $pi['basename'];
// convert to windows-style backslashes
if ($remote_path)
{
$remote_path = str_replace (DIRECTORY_SEPARATOR, '\\', $remote_path);
$cmd = "cd \"$remote_path\"; del \"$basename\"";
}
else
{
$cmd = "del \"$basename\"";
}
$retval = $this->execute ($cmd);
return $retval;
}
/**
* Makes a directory
*
* @param string $remote_path (use the local system's directory separators)
* @return bool true if successful, false otherwise
*/
public function mkdir ($remote_path)
{
$remote_path = str_replace (DIRECTORY_SEPARATOR, '\\', $remote_path);
$cmd = "mkdir \"$remote_path\"";
$retval = $this->execute ($cmd);
return $retval;
}
/**
* Lists the contents of a directory on the remote server;
* Results are returned in an array of arrays. Each subarray has the
* following hash key values:
*
* filename - name of the file
* size - size in bytes
* mtime - UNIX timestamp of file's modification time
* isdir - boolean indicating whether the file is a directory
*
* Note -- parsing smbclient "dir" output is inexact. Filenames with leading
* or trailing whitespace will lose these characters.
*
* @param string $remote_path
* @return mixed array of results if successful, false otherwise
*/
public function dir ($remote_path = '', $remote_filename = '')
{
// convert to windows-style backslashes
if ($remote_path)
{
$remote_path = str_replace (DIRECTORY_SEPARATOR, '\\', $remote_path);
if ($remote_filename)
{
$cmd = "cd \"$remote_path\"; dir \"{$remote_filename}\"";
}
else
{
$cmd = "cd \"$remote_path\"; dir";
}
}
else
{
if ($remote_filename)
{
$cmd = "dir \"{$remote_filename}\"";
}
else
{
$cmd = "dir";
}
}
$retval = $this->execute ($cmd);
if (!$retval)
{
return $retval;
}
$xary = array ();
foreach ($this->_last_cmd_stdout as $line)
{
if (!preg_match ('#\s+(.+?)\s+(.....)\s+(\d+)\s+(\w+\s+\w+\s+\d+\s+\d\d:\d\d:\d\d\s\d+)$#', $line, $matches))
{
continue;
}
list ($junk, $filename, $status, $size, $mtime) = $matches;
$filename = trim ($filename);
$status = trim ($status);
$mtime = strtotime($mtime);
$isdir = (stripos ($status, 'D') !== false) ? true : false;
$xary[] = array ('filename' => $filename, 'size' => $size, 'mtime' => $mtime, 'isdir' => $isdir);
}
return $xary;
}
private function execute ($cmd)
{
$this->build_full_cmd($cmd);
self::log_msg ($this->_cmd);
$outfile = tempnam(".", "cmd");
$errfile = tempnam(".", "cmd");
$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("file", $outfile, "w"),
2 => array("file", $errfile, "w")
);
$proc = proc_open($this->_cmd, $descriptorspec, $pipes);
if (!is_resource($proc)) return 255;
fclose($pipes[0]); //Don't really want to give any input
$exit = proc_close($proc);
$this->_last_cmd_stdout = file($outfile);
$this->_last_cmd_stderr = file($errfile);
$this->_last_cmd_exit_code = $exit;
self::log_msg ("exit code: " . $this->_last_cmd_exit_code);
self::log_msg ("stdout: " . join ("\n", $this->_last_cmd_stdout));
self::log_msg ("stderr: " . join ("\n", $this->_last_cmd_stderr));
unlink($outfile);
unlink($errfile);
if ($exit)
{
return false;
}
return true;
}
private function build_full_cmd ($cmd = '')
{
$this->_cmd = "smbclient '" . $this->_service . "'";
$this->_cmd .= " -U '" . $this->_username . "%" . $this->_password . "'";
if ($cmd)
{
$this->_cmd .= " -c '$cmd'";
}
}
/**
* Logs a message if debug_mode is true and if there is a global "log_msg" function.
* @param string $msg the message to log
*/
private static function log_msg ($msg)
{
if (!self::$debug_mode)
{
return;
}
if (function_exists ('log_msg'))
{
log_msg ('[' . self::$debug_label . "] $msg");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment