$this->_remove_from_stat_cache($filename);
$result = $this->_setstat_recursive($filename, $attr, $i);
$this->_read_put_responses($i);
// SFTPv4+ has an additional byte field - type - that would need to be sent, as well. setting it to
// SSH_FILEXFER_TYPE_UNKNOWN might work. if not, we'd have to do an SSH_FXP_STAT before doing an SSH_FXP_SETSTAT.
if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, pack('Na*a*', strlen($filename), $filename, $attr))) {
"Because some systems must use separate system calls to set various attributes, it is possible that a failure
response will be returned, but yet some of the attributes may be have been successfully modified. If possible,
servers SHOULD avoid this situation; however, clients MUST be aware that this is possible."
-- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.6
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_OK) {
$this->_logError($response, $status);
* Recursively sets information on directories on the SFTP server
* Minimizes directory lookups and SSH_FXP_STATUS requests for speed.
public function _setstat_recursive($path, $attr, &$i)
if (!$this->_read_put_responses($i)) {
$entries = $this->_list($path, true, false);
if ($entries === false) {
return $this->_setstat($path, $attr, false);
// normally $entries would have at least . and .. but it might not if the directories
// permissions didn't allow reading
foreach ($entries as $filename => $props) {
if ($filename == '.' || $filename == '..') {
if (!isset($props['type'])) {
$temp = $path.'/'.$filename;
if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) {
if (!$this->_setstat_recursive($temp, $attr, $i)) {
if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, pack('Na*a*', strlen($temp), $temp, $attr))) {
if ($i >= NET_SFTP_QUEUE_SIZE) {
if (!$this->_read_put_responses($i)) {
if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, pack('Na*a*', strlen($path), $path, $attr))) {
if ($i >= NET_SFTP_QUEUE_SIZE) {
if (!$this->_read_put_responses($i)) {
* Return the target of a symbolic link
public function readlink($link)
if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) {
$link = $this->_realpath($link);
if (!$this->_send_sftp_packet(NET_SFTP_READLINK, pack('Na*', strlen($link), $link))) {
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
$this->_logError($response);
user_error('Expected SSH_FXP_NAME or SSH_FXP_STATUS');
extract(unpack('Ncount', $this->_string_shift($response, 4)));
// the file isn't a symlink
extract(unpack('Nlength', $this->_string_shift($response, 4)));
return $this->_string_shift($response, $length);
* symboliclink() creates a symbolic link to the existing target with the specified name link.
* Warning: DO NOT call this function "s y m l i n k", the whole file gets deleted by some "very advanced" antivirus checker.
public function symboliclink($target, $link)
if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) {
$target = $this->_realpath($target);
$link = $this->_realpath($link);
$packet = pack('Na*Na*', strlen($target), $target, strlen($link), $link);
if (!$this->_send_sftp_packet(NET_SFTP_SYMLINK, $packet)) {
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_OK) {
$this->_logError($response, $status);
public function mkdir($dir, $mode = -1, $recursive = false)
if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) {
$dir = $this->_realpath($dir);
// by not providing any permissions, hopefully the server will use the logged in users umask - their
$attr = $mode == -1 ? "\0\0\0\0" : pack('N2', NET_SFTP_ATTR_PERMISSIONS, $mode & 07777);
$dirs = explode('/', preg_replace('#/(?=/)|/$#', '', $dir));
for ($i = 0; $i < count($dirs); $i++) {
$temp = array_slice($dirs, 0, $i + 1);
$temp = implode('/', $temp);
$result = $this->_mkdir_helper($temp, $attr);
return $this->_mkdir_helper($dir, $attr);
* Helper function for directory creation
public function _mkdir_helper($dir, $attr)
if (!$this->_send_sftp_packet(NET_SFTP_MKDIR, pack('Na*a*', strlen($dir), $dir, $attr))) {
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_OK) {
$this->_logError($response, $status);
public function rmdir($dir)
if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) {
$dir = $this->_realpath($dir);
if (!$this->_send_sftp_packet(NET_SFTP_RMDIR, pack('Na*', strlen($dir), $dir))) {
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');
extract(unpack('Nstatus', $this->_string_shift($response, 4)));
if ($status != NET_SFTP_STATUS_OK) {
// presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED?
$this->_logError($response, $status);
$this->_remove_from_stat_cache($dir);
// the following will do a soft delete, which would be useful if you deleted a file
// and then tried to do a stat on the deleted file. the above, in contrast, does
//$this->_update_stat_cache($dir, false);
* Uploads a file to the SFTP server.
* By default, Net_SFTP::put() does not read from the local filesystem. $data is dumped directly into $remote_file.
* So, for example, if you set $data to 'filename.ext' and then do Net_SFTP::get(), you will get a file, twelve bytes
* long, containing 'filename.ext' as its contents.
* Setting $mode to NET_SFTP_LOCAL_FILE will change the above behavior. With NET_SFTP_LOCAL_FILE, $remote_file will
* contain as many bytes as filename.ext does on your local filesystem. If your filename.ext is 1MB then that is how
* large $remote_file will be, as well.
* Currently, only binary mode is supported. As such, if the line endings need to be adjusted, you will need to take
* care of that, yourself.
* $mode can take an additional two parameters - NET_SFTP_RESUME and NET_SFTP_RESUME_START. These are bitwise AND'd with
* $mode. So if you want to resume upload of a 300mb file on the local file system you'd set $mode to the following:
* NET_SFTP_LOCAL_FILE | NET_SFTP_RESUME
* If you wanted to simply append the full contents of a local file to the full contents of a remote file you'd replace
* NET_SFTP_RESUME with NET_SFTP_RESUME_START.
* If $mode & (NET_SFTP_RESUME | NET_SFTP_RESUME_START) then NET_SFTP_RESUME_START will be assumed.
* $start and $local_start give you more fine grained control over this process and take precident over NET_SFTP_RESUME
* when they're non-negative. ie. $start could let you write at the end of a file (like NET_SFTP_RESUME) or in the middle
* of one. $local_start could let you start your reading from the end of a file (like NET_SFTP_RESUME_START) or in the
* Setting $local_start to > 0 or $mode | NET_SFTP_RESUME_START doesn't do anything unless $mode | NET_SFTP_LOCAL_FILE.
* @param String $remote_file
* @param optional Integer $mode
* @param optional Integer $start
* @param optional Integer $local_start
* @internal ASCII mode for SFTPv4/5/6 can be supported by adding a new function - Net_SFTP::setMode().
public function put($remote_file, $data, $mode = NET_SFTP_STRING, $start = -1, $local_start = -1)
if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) {
$remote_file = $this->_realpath($remote_file);
if ($remote_file === false) {
$this->_remove_from_stat_cache($remote_file);
$flags = NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE;
// according to the SFTP specs, NET_SFTP_OPEN_APPEND should "force all writes to append data at the end of the file."
// in practice, it doesn't seem to do that.
//$flags|= ($mode & NET_SFTP_RESUME) ? NET_SFTP_OPEN_APPEND : NET_SFTP_OPEN_TRUNCATE;
} elseif ($mode & NET_SFTP_RESUME) {
// if NET_SFTP_OPEN_APPEND worked as it should _size() wouldn't need to be called
$size = $this->size($remote_file);
$offset = $size !== false ? $size : 0;
$flags |= NET_SFTP_OPEN_TRUNCATE;
$packet = pack('Na*N2', strlen($remote_file), $remote_file, $flags, 0);
if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
$handle = substr($response, 4);
$this->_logError($response);
user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
// http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.3
if ($mode & NET_SFTP_LOCAL_FILE) {
user_error("$data is not a valid file");
$fp = @fopen($data, 'rb');
fseek($fp, $local_start);
} elseif ($mode & NET_SFTP_RESUME_START) {
$size = $size < 0 ? ($size & 0x7FFFFFFF) + 0x80000000 : $size;
$sftp_packet_size = 4096; // PuTTY uses 4096
// make the SFTP packet be exactly 4096 bytes by including the bytes in the NET_SFTP_WRITE packets "header"
$sftp_packet_size -= strlen($handle) + 25;
$temp = $mode & NET_SFTP_LOCAL_FILE ? fread($fp, $sftp_packet_size) : substr($data, $sent, $sftp_packet_size);
$subtemp = $offset + $sent;
$packet = pack('Na*N3a*', strlen($handle), $handle, $subtemp / 4294967296, $subtemp, strlen($temp), $temp);
if (!$this->_send_sftp_packet(NET_SFTP_WRITE, $packet)) {
if ($i == NET_SFTP_QUEUE_SIZE) {
if (!$this->_read_put_responses($i)) {
if (!$this->_read_put_responses($i)) {
if ($mode & NET_SFTP_LOCAL_FILE) {
$this->_close_handle($handle);
if ($mode & NET_SFTP_LOCAL_FILE) {
return $this->_close_handle($handle);
* Reads multiple successive SSH_FXP_WRITE responses
* Sending an SSH_FXP_WRITE packet and immediately reading its response isn't as efficient as blindly sending out $i
* SSH_FXP_WRITEs, in succession, and then reading $i responses.
public function _read_put_responses($i)
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS');