summaryrefslogtreecommitdiffstats
path: root/xbmc/filesystem/NFSFile.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/filesystem/NFSFile.cpp')
-rw-r--r--xbmc/filesystem/NFSFile.cpp962
1 files changed, 962 insertions, 0 deletions
diff --git a/xbmc/filesystem/NFSFile.cpp b/xbmc/filesystem/NFSFile.cpp
new file mode 100644
index 0000000..5e0c148
--- /dev/null
+++ b/xbmc/filesystem/NFSFile.cpp
@@ -0,0 +1,962 @@
+/*
+ * Copyright (C) 2011-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+// FileNFS.cpp: implementation of the CNFSFile class.
+//
+//////////////////////////////////////////////////////////////////////
+
+#include "NFSFile.h"
+
+#include "ServiceBroker.h"
+#include "network/DNSNameCache.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include <inttypes.h>
+#include <mutex>
+
+#include <nfsc/libnfs-raw-mount.h>
+#include <nfsc/libnfs.h>
+
+#ifdef TARGET_WINDOWS
+#include <fcntl.h>
+#include <sys\stat.h>
+#endif
+
+#if defined(TARGET_WINDOWS)
+#define S_IRGRP 0
+#define S_IROTH 0
+#define S_IWUSR _S_IWRITE
+#define S_IRUSR _S_IREAD
+#endif
+
+using namespace XFILE;
+
+using namespace std::chrono_literals;
+
+namespace
+{
+// Default "lease_time" on most Linux NFSv4 servers are 90s.
+// See: https://linux-nfs.org/wiki/index.php/NFS_lock_recovery_notes
+// Keep alive interval should be always less than lease_time to avoid client session expires
+
+constexpr auto CONTEXT_TIMEOUT = 60s; // 2/3 parts of lease_time
+constexpr auto KEEP_ALIVE_TIMEOUT = 45s; // half of lease_time
+constexpr auto IDLE_TIMEOUT = 30s; // close fast unused contexts when no active connections
+
+constexpr int NFS4ERR_EXPIRED = -11; // client session expired due idle time greater than lease_time
+
+constexpr auto SETTING_NFS_VERSION = "nfs.version";
+} // unnamed namespace
+
+CNfsConnection::CNfsConnection()
+ : m_pNfsContext(NULL),
+ m_exportPath(""),
+ m_hostName(""),
+ m_resolvedHostName(""),
+ m_IdleTimeout(std::chrono::steady_clock::now() + IDLE_TIMEOUT)
+{
+}
+
+CNfsConnection::~CNfsConnection()
+{
+ Deinit();
+}
+
+void CNfsConnection::resolveHost(const CURL& url)
+{
+ // resolve if hostname has changed
+ CDNSNameCache::Lookup(url.GetHostName(), m_resolvedHostName);
+}
+
+std::list<std::string> CNfsConnection::GetExportList(const CURL& url)
+{
+ std::list<std::string> retList;
+
+ struct exportnode *exportlist, *tmp;
+#ifdef HAS_NFS_MOUNT_GETEXPORTS_TIMEOUT
+ exportlist = mount_getexports_timeout(
+ m_resolvedHostName.c_str(),
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_nfsTimeout * 1000);
+#else
+ exportlist = mount_getexports(m_resolvedHostName.c_str());
+#endif
+
+ for (tmp = exportlist; tmp != NULL; tmp = tmp->ex_next)
+ {
+ std::string exportStr = std::string(tmp->ex_dir);
+
+ retList.push_back(exportStr);
+ }
+
+ mount_free_export_list(exportlist);
+ retList.sort();
+ retList.reverse();
+
+ return retList;
+}
+
+void CNfsConnection::clearMembers()
+{
+ // NOTE - DON'T CLEAR m_exportList HERE!
+ // splitUrlIntoExportAndPath checks for m_exportList.empty()
+ // and would query the server in an excessive unwanted fashion
+ // also don't clear m_KeepAliveTimeouts here because we
+ // would loose any "paused" file handles during export change
+ m_exportPath.clear();
+ m_hostName.clear();
+ m_writeChunkSize = 0;
+ m_readChunkSize = 0;
+ m_pNfsContext = NULL;
+}
+
+void CNfsConnection::destroyOpenContexts()
+{
+ std::unique_lock<CCriticalSection> lock(openContextLock);
+ for (auto& it : m_openContextMap)
+ {
+ nfs_destroy_context(it.second.pContext);
+ }
+ m_openContextMap.clear();
+}
+
+void CNfsConnection::destroyContext(const std::string &exportName)
+{
+ std::unique_lock<CCriticalSection> lock(openContextLock);
+ tOpenContextMap::iterator it = m_openContextMap.find(exportName.c_str());
+ if (it != m_openContextMap.end())
+ {
+ nfs_destroy_context(it->second.pContext);
+ m_openContextMap.erase(it);
+ }
+}
+
+struct nfs_context *CNfsConnection::getContextFromMap(const std::string &exportname, bool forceCacheHit/* = false*/)
+{
+ struct nfs_context *pRet = NULL;
+ std::unique_lock<CCriticalSection> lock(openContextLock);
+
+ tOpenContextMap::iterator it = m_openContextMap.find(exportname.c_str());
+ if (it != m_openContextMap.end())
+ {
+ //check if context has timed out already
+ auto now = std::chrono::steady_clock::now();
+ auto duration =
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - it->second.lastAccessedTime);
+ if (duration < CONTEXT_TIMEOUT || forceCacheHit)
+ {
+ //its not timedout yet or caller wants the cached entry regardless of timeout
+ //refresh access time of that
+ //context and return it
+ if (!forceCacheHit) // only log it if this isn't the resetkeepalive on each read ;)
+ CLog::Log(LOGDEBUG, "NFS: Refreshing context for {}, old: {}, new: {}", exportname,
+ it->second.lastAccessedTime.time_since_epoch().count(),
+ now.time_since_epoch().count());
+ it->second.lastAccessedTime = now;
+ pRet = it->second.pContext;
+ }
+ else
+ {
+ //context is timed out
+ //destroy it and return NULL
+ CLog::Log(LOGDEBUG, "NFS: Old context timed out - destroying it");
+ nfs_destroy_context(it->second.pContext);
+ m_openContextMap.erase(it);
+ }
+ }
+ return pRet;
+}
+
+CNfsConnection::ContextStatus CNfsConnection::getContextForExport(const std::string& exportname)
+{
+ CNfsConnection::ContextStatus ret = CNfsConnection::ContextStatus::INVALID;
+
+ clearMembers();
+
+ m_pNfsContext = getContextFromMap(exportname);
+
+ if(!m_pNfsContext)
+ {
+ CLog::Log(LOGDEBUG, "NFS: Context for {} not open - get a new context.", exportname);
+ m_pNfsContext = nfs_init_context();
+
+ if(!m_pNfsContext)
+ {
+ CLog::Log(LOGERROR,"NFS: Error initcontext in getContextForExport.");
+ }
+ else
+ {
+ struct contextTimeout tmp;
+ std::unique_lock<CCriticalSection> lock(openContextLock);
+ setOptions(m_pNfsContext);
+ tmp.pContext = m_pNfsContext;
+ tmp.lastAccessedTime = std::chrono::steady_clock::now();
+ m_openContextMap[exportname] = tmp; //add context to list of all contexts
+ ret = CNfsConnection::ContextStatus::NEW;
+ }
+ }
+ else
+ {
+ ret = CNfsConnection::ContextStatus::CACHED;
+ CLog::Log(LOGDEBUG,"NFS: Using cached context.");
+ }
+ m_lastAccessedTime = std::chrono::steady_clock::now();
+
+ return ret;
+}
+
+bool CNfsConnection::splitUrlIntoExportAndPath(const CURL& url, std::string &exportPath, std::string &relativePath)
+{
+ //refresh exportlist if empty or hostname change
+ if(m_exportList.empty() || !StringUtils::EqualsNoCase(url.GetHostName(), m_hostName))
+ {
+ const auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return false;
+
+ const auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return false;
+
+ const int nfsVersion = settings->GetInt(SETTING_NFS_VERSION);
+
+ if (nfsVersion == 4)
+ m_exportList = {"/"};
+ else
+ m_exportList = GetExportList(url);
+ }
+
+ return splitUrlIntoExportAndPath(url, exportPath, relativePath, m_exportList);
+}
+
+bool CNfsConnection::splitUrlIntoExportAndPath(const CURL& url,std::string &exportPath, std::string &relativePath, std::list<std::string> &exportList)
+{
+ bool ret = false;
+
+ if(!exportList.empty())
+ {
+ relativePath = "";
+ exportPath = "";
+
+ std::string path = url.GetFileName();
+
+ //GetFileName returns path without leading "/"
+ //but we need it because the export paths start with "/"
+ //and path.Find(*it) wouldn't work else
+ if(path[0] != '/')
+ {
+ path = "/" + path;
+ }
+
+ for (const std::string& it : exportList)
+ {
+ //if path starts with the current export path
+ if (URIUtils::PathHasParent(path, it))
+ {
+ /* It's possible that PathHasParent() may not find the correct match first/
+ * As an example, if /path/ & and /path/sub/ are exported, but
+ * the user specifies the path /path/subdir/ (from /path/ export).
+ * If the path is longer than the exportpath, make sure / is next.
+ */
+ if ((path.length() > it.length()) && (path[it.length()] != '/') && it != "/")
+ continue;
+ exportPath = it;
+ //handle special case where root is exported
+ //in that case we don't want to strip off to
+ //much from the path
+ if( exportPath == path )
+ relativePath = "//";
+ else if( exportPath == "/" )
+ relativePath = "//" + path.substr(exportPath.length());
+ else
+ relativePath = "//" + path.substr(exportPath.length()+1);
+ ret = true;
+ break;
+ }
+ }
+ }
+ return ret;
+}
+
+bool CNfsConnection::Connect(const CURL& url, std::string &relativePath)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ int nfsRet = 0;
+ std::string exportPath;
+
+ resolveHost(url);
+ bool ret = splitUrlIntoExportAndPath(url, exportPath, relativePath);
+
+ auto now = std::chrono::steady_clock::now();
+ auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastAccessedTime);
+
+ if ((ret && (exportPath != m_exportPath || url.GetHostName() != m_hostName)) ||
+ duration > CONTEXT_TIMEOUT)
+ {
+ CNfsConnection::ContextStatus contextRet = getContextForExport(url.GetHostName() + exportPath);
+
+ // we need a new context because sharename or hostname has changed
+ if (contextRet == CNfsConnection::ContextStatus::INVALID)
+ {
+ return false;
+ }
+
+ // new context was created - we need to mount it
+ if (contextRet == CNfsConnection::ContextStatus::NEW)
+ {
+ //we connect to the directory of the path. This will be the "root" path of this connection then.
+ //So all fileoperations are relative to this mountpoint...
+ nfsRet = nfs_mount(m_pNfsContext, m_resolvedHostName.c_str(), exportPath.c_str());
+
+ if(nfsRet != 0)
+ {
+ CLog::Log(LOGERROR, "NFS: Failed to mount nfs share: {} ({})", exportPath,
+ nfs_get_error(m_pNfsContext));
+ destroyContext(url.GetHostName() + exportPath);
+ return false;
+ }
+ CLog::Log(LOGDEBUG, "NFS: Connected to server {} and export {}", url.GetHostName(),
+ exportPath);
+ }
+ m_exportPath = exportPath;
+ m_hostName = url.GetHostName();
+
+ // read chunksize only works after mount
+ m_readChunkSize = nfs_get_readmax(m_pNfsContext);
+ m_writeChunkSize = nfs_get_writemax(m_pNfsContext);
+
+ if (m_readChunkSize == 0)
+ {
+ CLog::Log(LOGDEBUG, "NFS Server did not return max read chunksize - Using 128K default");
+ m_readChunkSize = 128 * 1024; // 128K
+ }
+ if (m_writeChunkSize == 0)
+ {
+ CLog::Log(LOGDEBUG, "NFS Server did not return max write chunksize - Using 128K default");
+ m_writeChunkSize = 128 * 1024; // 128K
+ }
+
+ if (contextRet == CNfsConnection::ContextStatus::NEW)
+ {
+ CLog::Log(LOGDEBUG, "NFS: chunks: r/w {}/{}", (int)m_readChunkSize, (int)m_writeChunkSize);
+ }
+ }
+ return ret;
+}
+
+void CNfsConnection::Deinit()
+{
+ if(m_pNfsContext)
+ {
+ destroyOpenContexts();
+ m_pNfsContext = NULL;
+ }
+ clearMembers();
+ // clear any keep alive timeouts on deinit
+ m_KeepAliveTimeouts.clear();
+}
+
+/* This is called from CApplication::ProcessSlow() and is used to tell if nfs have been idle for too long */
+void CNfsConnection::CheckIfIdle()
+{
+ /* We check if there are open connections. This is done without a lock to not halt the mainthread. It should be thread safe as
+ worst case scenario is that m_OpenConnections could read 0 and then changed to 1 if this happens it will enter the if which will lead to another check, which is locked. */
+ if (m_OpenConnections == 0 && m_pNfsContext != NULL)
+ { /* I've set the the maximum IDLE time to be 1 min and 30 sec. */
+ std::unique_lock<CCriticalSection> lock(*this);
+ if (m_OpenConnections == 0 /* check again - when locked */)
+ {
+ const auto now = std::chrono::steady_clock::now();
+
+ if (m_IdleTimeout < now)
+ {
+ CLog::Log(LOGINFO, "NFS is idle. Closing the remaining connections.");
+ gNfsConnection.Deinit();
+ }
+ }
+ }
+
+ if( m_pNfsContext != NULL )
+ {
+ std::unique_lock<CCriticalSection> lock(keepAliveLock);
+
+ const auto now = std::chrono::steady_clock::now();
+
+ //handle keep alive on opened files
+ for (auto& it : m_KeepAliveTimeouts)
+ {
+ if (it.second.refreshTime < now)
+ {
+ keepAlive(it.second.exportPath, it.first);
+ //reset timeout
+ resetKeepAlive(it.second.exportPath, it.first);
+ }
+ }
+ }
+}
+
+//remove file handle from keep alive list on file close
+void CNfsConnection::removeFromKeepAliveList(struct nfsfh *_pFileHandle)
+{
+ std::unique_lock<CCriticalSection> lock(keepAliveLock);
+ m_KeepAliveTimeouts.erase(_pFileHandle);
+}
+
+//reset timeouts on read
+void CNfsConnection::resetKeepAlive(const std::string& _exportPath, struct nfsfh* _pFileHandle)
+{
+ std::unique_lock<CCriticalSection> lock(keepAliveLock);
+ //refresh last access time of the context aswell
+ struct nfs_context *pContext = getContextFromMap(_exportPath, true);
+
+ // if we keep alive using m_pNfsContext we need to mark
+ // its last access time too here
+ if (m_pNfsContext == pContext)
+ {
+ m_lastAccessedTime = std::chrono::steady_clock::now();
+ }
+
+ //adds new keys - refreshes existing ones
+ m_KeepAliveTimeouts[_pFileHandle].exportPath = _exportPath;
+ m_KeepAliveTimeouts[_pFileHandle].refreshTime = m_lastAccessedTime + KEEP_ALIVE_TIMEOUT;
+}
+
+//keep alive the filehandles nfs connection
+//by blindly doing a read 32bytes - seek back to where
+//we were before
+void CNfsConnection::keepAlive(const std::string& _exportPath, struct nfsfh* _pFileHandle)
+{
+ uint64_t offset = 0;
+ char buffer[32];
+ // this also refreshes the last accessed time for the context
+ // true forces a cachehit regardless the context is timedout
+ // on this call we are sure its not timedout even if the last accessed
+ // time suggests it.
+ struct nfs_context *pContext = getContextFromMap(_exportPath, true);
+
+ if (!pContext)// this should normally never happen - paranoia
+ pContext = m_pNfsContext;
+
+ CLog::LogF(LOGDEBUG, "sending keep alive after {}s.",
+ std::chrono::duration_cast<std::chrono::seconds>(KEEP_ALIVE_TIMEOUT).count());
+
+ std::unique_lock<CCriticalSection> lock(*this);
+
+ nfs_lseek(pContext, _pFileHandle, 0, SEEK_CUR, &offset);
+
+ int bytes = nfs_read(pContext, _pFileHandle, 32, buffer);
+ if (bytes < 0)
+ {
+ CLog::LogF(LOGERROR, "nfs_read - Error ({}, {})", bytes, nfs_get_error(pContext));
+ return;
+ }
+
+ nfs_lseek(pContext, _pFileHandle, offset, SEEK_SET, &offset);
+}
+
+int CNfsConnection::stat(const CURL& url, nfs_stat_64* statbuff)
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ int nfsRet = 0;
+ std::string exportPath;
+ std::string relativePath;
+ struct nfs_context *pTmpContext = NULL;
+
+ resolveHost(url);
+
+ if(splitUrlIntoExportAndPath(url, exportPath, relativePath))
+ {
+ pTmpContext = nfs_init_context();
+
+ if(pTmpContext)
+ {
+ setOptions(pTmpContext);
+ //we connect to the directory of the path. This will be the "root" path of this connection then.
+ //So all fileoperations are relative to this mountpoint...
+ nfsRet = nfs_mount(pTmpContext, m_resolvedHostName.c_str(), exportPath.c_str());
+
+ if(nfsRet == 0)
+ {
+ nfsRet = nfs_stat64(pTmpContext, relativePath.c_str(), statbuff);
+ }
+ else
+ {
+ CLog::Log(LOGERROR, "NFS: Failed to mount nfs share: {} ({})", exportPath,
+ nfs_get_error(m_pNfsContext));
+ }
+
+ nfs_destroy_context(pTmpContext);
+ CLog::Log(LOGDEBUG, "NFS: Connected to server {} and export {} in tmpContext",
+ url.GetHostName(), exportPath);
+ }
+ }
+ return nfsRet;
+}
+
+/* The following two function is used to keep track on how many Opened files/directories there are.
+needed for unloading the dylib*/
+void CNfsConnection::AddActiveConnection()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ m_OpenConnections++;
+}
+
+void CNfsConnection::AddIdleConnection()
+{
+ std::unique_lock<CCriticalSection> lock(*this);
+ m_OpenConnections--;
+ /* If we close a file we reset the idle timer so that we don't have any weird behaviours if a user
+ leaves the movie paused for a long while and then press stop */
+ const auto now = std::chrono::steady_clock::now();
+ m_IdleTimeout = now + IDLE_TIMEOUT;
+}
+
+
+void CNfsConnection::setOptions(struct nfs_context* context)
+{
+ const auto settingsComponent = CServiceBroker::GetSettingsComponent();
+ if (!settingsComponent)
+ return;
+
+ const auto advancedSettings = settingsComponent->GetAdvancedSettings();
+ if (!advancedSettings)
+ return;
+
+#ifdef HAS_NFS_SET_TIMEOUT
+ uint32_t timeout = advancedSettings->m_nfsTimeout;
+ nfs_set_timeout(context, timeout > 0 ? timeout * 1000 : -1);
+#endif
+ int retries = advancedSettings->m_nfsRetries;
+ nfs_set_autoreconnect(context, retries);
+
+ const auto settings = settingsComponent->GetSettings();
+ if (!settings)
+ return;
+
+ const int nfsVersion = settings->GetInt(SETTING_NFS_VERSION);
+
+ int ret = nfs_set_version(context, nfsVersion);
+ if (ret != 0)
+ {
+ CLog::Log(LOGERROR, "NFS: Failed to set nfs version: {} ({})", nfsVersion,
+ nfs_get_error(context));
+ return;
+ }
+
+ CLog::Log(LOGDEBUG, "NFS: version: {}", nfsVersion);
+}
+
+CNfsConnection gNfsConnection;
+
+CNFSFile::CNFSFile()
+: m_pFileHandle(NULL)
+, m_pNfsContext(NULL)
+{
+ gNfsConnection.AddActiveConnection();
+}
+
+CNFSFile::~CNFSFile()
+{
+ Close();
+ gNfsConnection.AddIdleConnection();
+}
+
+int64_t CNFSFile::GetPosition()
+{
+ int ret = 0;
+ uint64_t offset = 0;
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+
+ if (gNfsConnection.GetNfsContext() == NULL || m_pFileHandle == NULL) return 0;
+
+ ret = nfs_lseek(gNfsConnection.GetNfsContext(), m_pFileHandle, 0, SEEK_CUR, &offset);
+
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "NFS: Failed to lseek({})", nfs_get_error(gNfsConnection.GetNfsContext()));
+ }
+ return offset;
+}
+
+int64_t CNFSFile::GetLength()
+{
+ if (m_pFileHandle == NULL) return 0;
+ return m_fileSize;
+}
+
+bool CNFSFile::Open(const CURL& url)
+{
+ Close();
+ // we can't open files like nfs://file.f or nfs://server/file.f
+ // if a file matches the if below return false, it can't exist on a nfs share.
+ if (!IsValidFile(url.GetFileName()))
+ {
+ CLog::Log(LOGINFO, "NFS: Bad URL : '{}'", url.GetFileName());
+ return false;
+ }
+
+ std::string filename;
+
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+
+ if (!gNfsConnection.Connect(url, filename))
+ return false;
+
+ m_pNfsContext = gNfsConnection.GetNfsContext();
+ m_exportPath = gNfsConnection.GetContextMapId();
+
+ int ret = nfs_open(m_pNfsContext, filename.c_str(), O_RDONLY, &m_pFileHandle);
+
+ if (ret == NFS4ERR_EXPIRED) // client session expired due no activity/keep alive
+ {
+ CLog::Log(LOGERROR,
+ "CNFSFile::Open: Unable to open file - trying again with a new context: error: '{}'",
+ nfs_get_error(m_pNfsContext));
+
+ gNfsConnection.Deinit();
+ m_pNfsContext = gNfsConnection.GetNfsContext();
+ m_exportPath = gNfsConnection.GetContextMapId();
+ ret = nfs_open(m_pNfsContext, filename.c_str(), O_RDONLY, &m_pFileHandle);
+ }
+
+ if (ret != 0)
+ {
+ CLog::Log(LOGERROR, "CNFSFile::Open: Unable to open file: '{}' error: '{}'", url.GetFileName(),
+ nfs_get_error(m_pNfsContext));
+
+ m_pNfsContext = nullptr;
+ m_exportPath.clear();
+ return false;
+ }
+
+ CLog::Log(LOGDEBUG, "CNFSFile::Open - opened {}", url.GetFileName());
+ m_url=url;
+
+ struct __stat64 tmpBuffer;
+
+ if( Stat(&tmpBuffer) )
+ {
+ m_url.Reset();
+ Close();
+ return false;
+ }
+
+ m_fileSize = tmpBuffer.st_size;//cache the size of this file
+ // We've successfully opened the file!
+ return true;
+}
+
+bool CNFSFile::Exists(const CURL& url)
+{
+ return Stat(url,NULL) == 0;
+}
+
+int CNFSFile::Stat(struct __stat64* buffer)
+{
+ return Stat(m_url,buffer);
+}
+
+
+int CNFSFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ int ret = 0;
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ std::string filename;
+
+ if(!gNfsConnection.Connect(url,filename))
+ return -1;
+
+ nfs_stat_64 tmpBuffer = {};
+
+ ret = nfs_stat64(gNfsConnection.GetNfsContext(), filename.c_str(), &tmpBuffer);
+
+ //if buffer == NULL we where called from Exists - in that case don't spam the log with errors
+ if (ret != 0 && buffer != NULL)
+ {
+ CLog::Log(LOGERROR, "NFS: Failed to stat({}) {}", url.GetFileName(),
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ ret = -1;
+ }
+ else
+ {
+ if (buffer)
+ {
+ *buffer = {};
+ buffer->st_dev = tmpBuffer.nfs_dev;
+ buffer->st_ino = tmpBuffer.nfs_ino;
+ buffer->st_mode = tmpBuffer.nfs_mode;
+ buffer->st_nlink = tmpBuffer.nfs_nlink;
+ buffer->st_uid = tmpBuffer.nfs_uid;
+ buffer->st_gid = tmpBuffer.nfs_gid;
+ buffer->st_rdev = tmpBuffer.nfs_rdev;
+ buffer->st_size = tmpBuffer.nfs_size;
+ buffer->st_atime = tmpBuffer.nfs_atime;
+ buffer->st_mtime = tmpBuffer.nfs_mtime;
+ buffer->st_ctime = tmpBuffer.nfs_ctime;
+ }
+ }
+ return ret;
+}
+
+ssize_t CNFSFile::Read(void *lpBuf, size_t uiBufSize)
+{
+ if (uiBufSize > SSIZE_MAX)
+ uiBufSize = SSIZE_MAX;
+
+ ssize_t numberOfBytesRead = 0;
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+
+ if (m_pFileHandle == NULL || m_pNfsContext == NULL )
+ return -1;
+
+ numberOfBytesRead = nfs_read(m_pNfsContext, m_pFileHandle, uiBufSize, (char *)lpBuf);
+
+ lock.unlock(); //no need to keep the connection lock after that
+
+ gNfsConnection.resetKeepAlive(m_exportPath, m_pFileHandle);//triggers keep alive timer reset for this filehandle
+
+ //something went wrong ...
+ if (numberOfBytesRead < 0)
+ CLog::Log(LOGERROR, "{} - Error( {}, {} )", __FUNCTION__, (int64_t)numberOfBytesRead,
+ nfs_get_error(m_pNfsContext));
+
+ return numberOfBytesRead;
+}
+
+int64_t CNFSFile::Seek(int64_t iFilePosition, int iWhence)
+{
+ int ret = 0;
+ uint64_t offset = 0;
+
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ if (m_pFileHandle == NULL || m_pNfsContext == NULL) return -1;
+
+
+ ret = nfs_lseek(m_pNfsContext, m_pFileHandle, iFilePosition, iWhence, &offset);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "{} - Error( seekpos: {}, whence: {}, fsize: {}, {})", __FUNCTION__,
+ iFilePosition, iWhence, m_fileSize, nfs_get_error(m_pNfsContext));
+ return -1;
+ }
+ return (int64_t)offset;
+}
+
+int CNFSFile::Truncate(int64_t iSize)
+{
+ int ret = 0;
+
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ if (m_pFileHandle == NULL || m_pNfsContext == NULL) return -1;
+
+
+ ret = nfs_ftruncate(m_pNfsContext, m_pFileHandle, iSize);
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "{} - Error( ftruncate: {}, fsize: {}, {})", __FUNCTION__, iSize,
+ m_fileSize, nfs_get_error(m_pNfsContext));
+ return -1;
+ }
+ return ret;
+}
+
+void CNFSFile::Close()
+{
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+
+ if (m_pFileHandle != NULL && m_pNfsContext != NULL)
+ {
+ int ret = 0;
+ CLog::Log(LOGDEBUG, "CNFSFile::Close closing file {}", m_url.GetFileName());
+ // remove it from keep alive list before closing
+ // so keep alive code doesn't process it anymore
+ gNfsConnection.removeFromKeepAliveList(m_pFileHandle);
+ ret = nfs_close(m_pNfsContext, m_pFileHandle);
+
+ if (ret < 0)
+ {
+ CLog::Log(LOGERROR, "Failed to close({}) - {}", m_url.GetFileName(),
+ nfs_get_error(m_pNfsContext));
+ }
+ m_pFileHandle = NULL;
+ m_pNfsContext = NULL;
+ m_fileSize = 0;
+ m_exportPath.clear();
+ }
+}
+
+//this was a bitch!
+//for nfs write to work we have to write chunked
+//otherwise this could crash on big files
+ssize_t CNFSFile::Write(const void* lpBuf, size_t uiBufSize)
+{
+ size_t numberOfBytesWritten = 0;
+ int writtenBytes = 0;
+ size_t leftBytes = uiBufSize;
+ //clamp max write chunksize to 32kb - fixme - this might be superfluous with future libnfs versions
+ size_t chunkSize = gNfsConnection.GetMaxWriteChunkSize() > 32768 ? 32768 : (size_t)gNfsConnection.GetMaxWriteChunkSize();
+
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+
+ if (m_pFileHandle == NULL || m_pNfsContext == NULL) return -1;
+
+ //write as long as some bytes are left to be written
+ while( leftBytes )
+ {
+ //the last chunk could be smalle than chunksize
+ if(leftBytes < chunkSize)
+ {
+ chunkSize = leftBytes;//write last chunk with correct size
+ }
+ //write chunk
+ //! @bug libnfs < 2.0.0 isn't const correct
+ writtenBytes = nfs_write(m_pNfsContext,
+ m_pFileHandle,
+ chunkSize,
+ const_cast<char*>((const char *)lpBuf) + numberOfBytesWritten);
+ //decrease left bytes
+ leftBytes-= writtenBytes;
+ //increase overall written bytes
+ numberOfBytesWritten += writtenBytes;
+
+ //danger - something went wrong
+ if (writtenBytes < 0)
+ {
+ CLog::Log(LOGERROR, "Failed to pwrite({}) {}", m_url.GetFileName(),
+ nfs_get_error(m_pNfsContext));
+ if (numberOfBytesWritten == 0)
+ return -1;
+
+ break;
+ }
+ }
+ //return total number of written bytes
+ return numberOfBytesWritten;
+}
+
+bool CNFSFile::Delete(const CURL& url)
+{
+ int ret = 0;
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ std::string filename;
+
+ if(!gNfsConnection.Connect(url, filename))
+ return false;
+
+
+ ret = nfs_unlink(gNfsConnection.GetNfsContext(), filename.c_str());
+
+ if(ret != 0)
+ {
+ CLog::Log(LOGERROR, "{} - Error( {} )", __FUNCTION__,
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ }
+ return (ret == 0);
+}
+
+bool CNFSFile::Rename(const CURL& url, const CURL& urlnew)
+{
+ int ret = 0;
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ std::string strFile;
+
+ if(!gNfsConnection.Connect(url,strFile))
+ return false;
+
+ std::string strFileNew;
+ std::string strDummy;
+ gNfsConnection.splitUrlIntoExportAndPath(urlnew, strDummy, strFileNew);
+
+ ret = nfs_rename(gNfsConnection.GetNfsContext() , strFile.c_str(), strFileNew.c_str());
+
+ if(ret != 0)
+ {
+ CLog::Log(LOGERROR, "{} - Error( {} )", __FUNCTION__,
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ }
+ return (ret == 0);
+}
+
+bool CNFSFile::OpenForWrite(const CURL& url, bool bOverWrite)
+{
+ int ret = 0;
+ // we can't open files like nfs://file.f or nfs://server/file.f
+ // if a file matches the if below return false, it can't exist on a nfs share.
+ if (!IsValidFile(url.GetFileName())) return false;
+
+ Close();
+ std::unique_lock<CCriticalSection> lock(gNfsConnection);
+ std::string filename;
+
+ if(!gNfsConnection.Connect(url,filename))
+ return false;
+
+ m_pNfsContext = gNfsConnection.GetNfsContext();
+ m_exportPath = gNfsConnection.GetContextMapId();
+
+ if (bOverWrite)
+ {
+ CLog::Log(LOGWARNING, "FileNFS::OpenForWrite() called with overwriting enabled! - {}",
+ filename);
+ //create file with proper permissions
+ ret = nfs_creat(m_pNfsContext, filename.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH, &m_pFileHandle);
+ //if file was created the file handle isn't valid ... so close it and open later
+ if(ret == 0)
+ {
+ nfs_close(m_pNfsContext,m_pFileHandle);
+ m_pFileHandle = NULL;
+ }
+ }
+
+ ret = nfs_open(m_pNfsContext, filename.c_str(), O_RDWR, &m_pFileHandle);
+
+ if (ret || m_pFileHandle == NULL)
+ {
+ // write error to logfile
+ CLog::Log(LOGERROR, "CNFSFile::Open: Unable to open file : '{}' error : '{}'", filename,
+ nfs_get_error(gNfsConnection.GetNfsContext()));
+ m_pNfsContext = NULL;
+ m_exportPath.clear();
+ return false;
+ }
+ m_url=url;
+
+ struct __stat64 tmpBuffer = {};
+
+ //only stat if file was not created
+ if(!bOverWrite)
+ {
+ if(Stat(&tmpBuffer))
+ {
+ m_url.Reset();
+ Close();
+ return false;
+ }
+ m_fileSize = tmpBuffer.st_size;//cache filesize of this file
+ }
+ else//file was created - filesize is zero
+ {
+ m_fileSize = 0;
+ }
+
+ // We've successfully opened the file!
+ return true;
+}
+
+bool CNFSFile::IsValidFile(const std::string& strFileName)
+{
+ if (strFileName.find('/') == std::string::npos || /* doesn't have sharename */
+ StringUtils::EndsWith(strFileName, "/.") || /* not current folder */
+ StringUtils::EndsWith(strFileName, "/..")) /* not parent folder */
+ return false;
+ return true;
+}