Partial fix for delete directory
This commit is contained in:
@@ -196,11 +196,12 @@ class SIADRIVE_DOKAN_EXPORTABLE DokanFindFiles :
|
|||||||
public CEvent
|
public CEvent
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DokanFindFiles(const SString& cachePath, const SString& rootPath, const SString& siaQuery, const SString& findFile) :
|
DokanFindFiles(const SString& cachePath, const SString& rootPath, const SString& siaQuery, const SString& findFile, const SString& fileName) :
|
||||||
_cachePath(cachePath),
|
_cachePath(cachePath),
|
||||||
_rootPath(rootPath),
|
_rootPath(rootPath),
|
||||||
_siaQuery(siaQuery),
|
_siaQuery(siaQuery),
|
||||||
_findFile(findFile)
|
_findFile(findFile),
|
||||||
|
_fileName(fileName)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,11 +215,12 @@ private:
|
|||||||
const SString _rootPath;
|
const SString _rootPath;
|
||||||
const SString _siaQuery;
|
const SString _siaQuery;
|
||||||
const SString _findFile;
|
const SString _findFile;
|
||||||
|
const SString _fileName;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual std::shared_ptr<CEvent> Clone() const override
|
virtual std::shared_ptr<CEvent> Clone() const override
|
||||||
{
|
{
|
||||||
return std::shared_ptr<CEvent>(new DokanFindFiles(_cachePath, _rootPath, _siaQuery, _findFile));
|
return std::shared_ptr<CEvent>(new DokanFindFiles(_cachePath, _rootPath, _siaQuery, _findFile, _fileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual SString GetSingleLineMessage() const override
|
virtual SString GetSingleLineMessage() const override
|
||||||
@@ -226,7 +228,8 @@ public:
|
|||||||
return L"DokanFindFiles|PATH|" + _cachePath +
|
return L"DokanFindFiles|PATH|" + _cachePath +
|
||||||
"|ROOT|" + _rootPath +
|
"|ROOT|" + _rootPath +
|
||||||
"|QUERY|" + _siaQuery +
|
"|QUERY|" + _siaQuery +
|
||||||
"|FIND|" + _findFile;
|
"|FIND|" + _findFile +
|
||||||
|
"|FN|" + _fileName;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -265,8 +268,10 @@ class SIADRIVE_DOKAN_EXPORTABLE DokanGetFileInformation :
|
|||||||
public CEvent
|
public CEvent
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DokanGetFileInformation(const SString& cachePath) :
|
DokanGetFileInformation(const SString& cachePath, const SString& fileName, const NTSTATUS& result) :
|
||||||
_cachePath(cachePath)
|
_cachePath(cachePath),
|
||||||
|
_fileName(fileName),
|
||||||
|
_result(result)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,16 +282,18 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
const SString _cachePath;
|
const SString _cachePath;
|
||||||
|
const SString _fileName;
|
||||||
|
const NTSTATUS _result;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual std::shared_ptr<CEvent> Clone() const override
|
virtual std::shared_ptr<CEvent> Clone() const override
|
||||||
{
|
{
|
||||||
return std::shared_ptr<CEvent>(new DokanGetFileInformation(_cachePath));
|
return std::shared_ptr<CEvent>(new DokanGetFileInformation(_cachePath, _fileName, _result));
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual SString GetSingleLineMessage() const override
|
virtual SString GetSingleLineMessage() const override
|
||||||
{
|
{
|
||||||
return L"DokanGetFileInformation|PATH|" + _cachePath;
|
return L"DokanGetFileInformation|PATH|" + _cachePath + "|FN|" + _fileName + "|RES|" + SString::FromUInt32(_result);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -274,7 +274,7 @@ private:
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// FILE_FLAG_BACKUP_SEMANTICS is required for opening directory handles
|
// FILE_FLAG_BACKUP_SEMANTICS is required for opening directory handles
|
||||||
HANDLE handle = ::CreateFile(&cacheFilePath[0], genericDesiredAccess, shareAccess, &securityAttrib, OPEN_EXISTING, fileAttributesAndFlags | FILE_FLAG_BACKUP_SEMANTICS, nullptr);
|
HANDLE handle = ::CreateFile(&cacheFilePath[0], genericDesiredAccess, shareAccess, &securityAttrib, OPEN_EXISTING, fileAttributesAndFlags | FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_POSIX_SEMANTICS, nullptr);
|
||||||
if (handle == INVALID_HANDLE_VALUE)
|
if (handle == INVALID_HANDLE_VALUE)
|
||||||
{
|
{
|
||||||
DWORD error = GetLastError();
|
DWORD error = GetLastError();
|
||||||
@@ -295,7 +295,7 @@ private:
|
|||||||
else // File (cache and/or Sia operation)
|
else // File (cache and/or Sia operation)
|
||||||
{
|
{
|
||||||
// Formulate Sia path
|
// Formulate Sia path
|
||||||
SString siaPath = CSiaApi::FormatToSiaPath(FilePath(fileName).SkipRoot()); // Strip drive letter to get Sia path
|
SString siaPath = CSiaApi::FormatToSiaPath(fileName); // Strip drive letter to get Sia path
|
||||||
if (siaPath.Length())
|
if (siaPath.Length())
|
||||||
{
|
{
|
||||||
// If cache file already exists and is a directory, requested file operation isn't valid
|
// If cache file already exists and is a directory, requested file operation isn't valid
|
||||||
@@ -474,37 +474,36 @@ private:
|
|||||||
|
|
||||||
static NTSTATUS DOKAN_CALLBACK Sia_FindFiles(LPCWSTR fileName, PFillFindData fillFindData, PDOKAN_FILE_INFO dokanFileInfo)
|
static NTSTATUS DOKAN_CALLBACK Sia_FindFiles(LPCWSTR fileName, PFillFindData fillFindData, PDOKAN_FILE_INFO dokanFileInfo)
|
||||||
{
|
{
|
||||||
NTSTATUS ret = STATUS_SUCCESS;
|
NTSTATUS ret = STATUS_INVALID_SERVER_STATE;
|
||||||
auto siaFileTree = GetFileTree();
|
auto siaFileTree = GetFileTree();
|
||||||
if (siaFileTree)
|
if (siaFileTree)
|
||||||
{
|
{
|
||||||
SString siaFileQuery = CSiaApi::FormatToSiaPath(FilePath(fileName).SkipRoot());
|
SString siaFileQuery = CSiaApi::FormatToSiaPath(fileName);
|
||||||
SString siaRootPath = CSiaApi::FormatToSiaPath(FilePath(fileName).SkipRoot());
|
SString siaRootPath = CSiaApi::FormatToSiaPath(fileName);
|
||||||
FilePath siaDirQuery = siaFileQuery;
|
FilePath siaDirQuery = siaFileQuery;
|
||||||
FilePath findFile = GetCacheLocation();
|
FilePath findFile = GetCacheLocation();
|
||||||
FilePath cachePath = GetCacheLocation();
|
FilePath cachePath = GetCacheLocation();
|
||||||
if (FilePath::DirSep == fileName)
|
if (FilePath::DirSep == fileName)
|
||||||
{
|
{
|
||||||
siaFileQuery += L"/*.*";
|
siaFileQuery += L"/*.*";
|
||||||
findFile.Append("*.*");
|
findFile.Append("*");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
cachePath.Append(fileName);
|
cachePath.Append(fileName);
|
||||||
findFile.Append(fileName);
|
findFile.Append(fileName);
|
||||||
if (cachePath.IsDirectory())
|
if (dokanFileInfo->IsDirectory)
|
||||||
{
|
{
|
||||||
siaFileQuery += L"/*.*";
|
siaFileQuery += L"/*.*";
|
||||||
findFile.Append("*.*");
|
findFile.Append("*");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
siaDirQuery = cachePath;
|
siaDirQuery = FilePath();
|
||||||
siaDirQuery = CSiaApi::FormatToSiaPath(siaDirQuery.RemoveFileName());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CEventSystem::EventSystem.NotifyEvent(CreateSystemEvent(DokanFindFiles(cachePath, siaDirQuery, siaFileQuery, findFile)));
|
CEventSystem::EventSystem.NotifyEvent(CreateSystemEvent(DokanFindFiles(cachePath, siaDirQuery, siaFileQuery, findFile, fileName)));
|
||||||
|
|
||||||
WIN32_FIND_DATA findData = { 0 };
|
WIN32_FIND_DATA findData = { 0 };
|
||||||
HANDLE findHandle = ::FindFirstFile(&findFile[0], &findData);
|
HANDLE findHandle = ::FindFirstFile(&findFile[0], &findData);
|
||||||
@@ -525,62 +524,79 @@ private:
|
|||||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||||
{
|
{
|
||||||
dirs.insert({ findData.cFileName, 0 });
|
dirs.insert({ findData.cFileName, 0 });
|
||||||
}
|
|
||||||
|
|
||||||
bool exists;
|
|
||||||
if (!ApiSuccess(_siaApi->GetRenter()->FileExists(CSiaApi::FormatToSiaPath(FilePath(siaRootPath, findData.cFileName)), exists)))
|
|
||||||
{
|
|
||||||
::FindClose(findHandle);
|
|
||||||
return STATUS_INVALID_DEVICE_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (findData.nFileSizeHigh || findData.nFileSizeLow || !exists)
|
|
||||||
{
|
|
||||||
fillFindData(&findData, dokanFileInfo);
|
fillFindData(&findData, dokanFileInfo);
|
||||||
files.insert({ findData.cFileName, 1 });
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bool exists;
|
||||||
|
if (!ApiSuccess(_siaApi->GetRenter()->FileExists(CSiaApi::FormatToSiaPath(FilePath(fileName, findData.cFileName)), exists)))
|
||||||
|
{
|
||||||
|
::FindClose(findHandle);
|
||||||
|
return STATUS_INVALID_DEVICE_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (findData.nFileSizeHigh || findData.nFileSizeLow || !exists)
|
||||||
|
{
|
||||||
|
fillFindData(&findData, dokanFileInfo);
|
||||||
|
files.insert({ findData.cFileName, 1 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (::FindNextFile(findHandle, &findData) != 0);
|
} while (::FindNextFile(findHandle, &findData) != 0);
|
||||||
|
|
||||||
|
DWORD error = GetLastError();
|
||||||
::FindClose(findHandle);
|
::FindClose(findHandle);
|
||||||
|
|
||||||
// Find Sia directories
|
if (error == ERROR_NO_MORE_FILES)
|
||||||
auto dirList = siaFileTree->QueryDirectories(siaDirQuery);
|
|
||||||
for (auto& dir : dirList)
|
|
||||||
{
|
{
|
||||||
if (dirs.find(dir) == dirs.end())
|
// Find Sia directories
|
||||||
|
if (!static_cast<SString>(siaDirQuery).IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
WIN32_FIND_DATA fd = { 0 };
|
auto dirList = siaFileTree->QueryDirectories(siaDirQuery);
|
||||||
wcscpy_s(fd.cFileName, dir.str().c_str());
|
for (auto& dir : dirList)
|
||||||
fd.dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY;
|
|
||||||
fillFindData(&fd, dokanFileInfo);
|
|
||||||
|
|
||||||
// Create cache sub-folder
|
|
||||||
FilePath subCachePath(cachePath, dir);
|
|
||||||
if (!subCachePath.IsDirectory())
|
|
||||||
{
|
{
|
||||||
subCachePath.CreateDirectory();
|
if (dirs.find(dir) == dirs.end())
|
||||||
|
{
|
||||||
|
// Create cache sub-folder
|
||||||
|
FilePath subCachePath(cachePath, dir);
|
||||||
|
if (!subCachePath.IsDirectory())
|
||||||
|
{
|
||||||
|
subCachePath.CreateDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
WIN32_FIND_DATA fd = { 0 };
|
||||||
|
wcscpy_s(fd.cFileName, dir.str().c_str());
|
||||||
|
fd.dwFileAttributes = FILE_ATTRIBUTE_DIRECTORY;
|
||||||
|
fillFindData(&fd, dokanFileInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Find Sia files
|
// Find Sia files
|
||||||
auto fileList = siaFileTree->Query(siaFileQuery);
|
auto fileList = siaFileTree->Query(siaFileQuery);
|
||||||
for (auto& file : fileList)
|
for (auto& file : fileList)
|
||||||
{
|
|
||||||
FilePath fp = file->GetSiaPath();
|
|
||||||
fp.StripToFileName();
|
|
||||||
if (files.find(fp) == files.end())
|
|
||||||
{
|
{
|
||||||
WIN32_FIND_DATA fd = { 0 };
|
FilePath fp = file->GetSiaPath();
|
||||||
wcscpy_s(fd.cFileName, &fp[0]);
|
fp.StripToFileName();
|
||||||
|
if (files.find(fp) == files.end())
|
||||||
|
{
|
||||||
|
WIN32_FIND_DATA fd = { 0 };
|
||||||
|
wcscpy_s(fd.cFileName, &fp[0]);
|
||||||
|
|
||||||
LARGE_INTEGER li = { 0 };
|
LARGE_INTEGER li = { 0 };
|
||||||
li.QuadPart = file->GetFileSize();
|
li.QuadPart = file->GetFileSize();
|
||||||
fd.nFileSizeHigh = li.HighPart;
|
fd.nFileSizeHigh = li.HighPart;
|
||||||
fd.nFileSizeLow = li.LowPart;
|
fd.nFileSizeLow = li.LowPart;
|
||||||
fd.dwFileAttributes = FILE_ATTRIBUTE_ARCHIVE | FILE_ATTRIBUTE_NORMAL;
|
fd.dwFileAttributes = FILE_ATTRIBUTE_ARCHIVE | FILE_ATTRIBUTE_NORMAL;
|
||||||
fillFindData(&fd, dokanFileInfo);
|
fillFindData(&fd, dokanFileInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ret = STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ret = DokanNtStatusFromWin32(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -612,17 +628,13 @@ private:
|
|||||||
BOOL opened = FALSE;
|
BOOL opened = FALSE;
|
||||||
NTSTATUS ret = STATUS_SUCCESS;
|
NTSTATUS ret = STATUS_SUCCESS;
|
||||||
|
|
||||||
FilePath cachePath(GetCacheLocation(), fileName);
|
|
||||||
|
|
||||||
SString siaPath = CSiaApi::FormatToSiaPath(FilePath(fileName).SkipRoot());
|
|
||||||
auto siaFileTree = GetFileTree();
|
auto siaFileTree = GetFileTree();
|
||||||
auto siaFile = siaFileTree ? siaFileTree->GetFile(siaPath) : nullptr;
|
auto siaFile = siaFileTree ? siaFileTree->GetFile(openFileInfo->SiaPath) : nullptr;
|
||||||
CEventSystem::EventSystem.NotifyEvent(CreateSystemEvent(DokanGetFileInformation(cachePath)));
|
|
||||||
|
|
||||||
HANDLE tempHandle = openFileInfo->FileHandle;
|
HANDLE tempHandle = openFileInfo->FileHandle;
|
||||||
if (!siaFile && (!openFileInfo->FileHandle || (openFileInfo->FileHandle == INVALID_HANDLE_VALUE)))
|
if (!siaFile && (!tempHandle || (tempHandle == INVALID_HANDLE_VALUE)))
|
||||||
{
|
{
|
||||||
tempHandle = ::CreateFile(&cachePath[0], GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
|
tempHandle = ::CreateFile(&openFileInfo->CacheFilePath[0], GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||||
if (tempHandle == INVALID_HANDLE_VALUE)
|
if (tempHandle == INVALID_HANDLE_VALUE)
|
||||||
{
|
{
|
||||||
ret = DokanNtStatusFromWin32(::GetLastError());
|
ret = DokanNtStatusFromWin32(::GetLastError());
|
||||||
@@ -650,12 +662,12 @@ private:
|
|||||||
// in this case, FindFirstFile can't get directory information
|
// in this case, FindFirstFile can't get directory information
|
||||||
if (wcscmp(fileName, L"\\") == 0)
|
if (wcscmp(fileName, L"\\") == 0)
|
||||||
{
|
{
|
||||||
handleFileInfo->dwFileAttributes = ::GetFileAttributes(&cachePath[0]);
|
handleFileInfo->dwFileAttributes = ::GetFileAttributes(&openFileInfo->CacheFilePath[0]);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WIN32_FIND_DATAW find = { 0 };
|
WIN32_FIND_DATAW find = { 0 };
|
||||||
HANDLE findHandle = ::FindFirstFile(&cachePath[0], &find);
|
HANDLE findHandle = ::FindFirstFile(&openFileInfo->CacheFilePath[0], &find);
|
||||||
if (findHandle == INVALID_HANDLE_VALUE)
|
if (findHandle == INVALID_HANDLE_VALUE)
|
||||||
{
|
{
|
||||||
DWORD error = ::GetLastError();
|
DWORD error = ::GetLastError();
|
||||||
@@ -681,6 +693,8 @@ private:
|
|||||||
::CloseHandle(tempHandle);
|
::CloseHandle(tempHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CEventSystem::EventSystem.NotifyEvent(CreateSystemEvent(DokanGetFileInformation(openFileInfo->CacheFilePath, fileName, ret)));
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,7 +723,7 @@ private:
|
|||||||
UNREFERENCED_PARAMETER(dokanFileInfo);
|
UNREFERENCED_PARAMETER(dokanFileInfo);
|
||||||
|
|
||||||
// TODO Implement this correctly
|
// TODO Implement this correctly
|
||||||
*FreeBytesAvailable = static_cast<ULONGLONG>(512 * 1024 * 1024);
|
*FreeBytesAvailable = static_cast<ULONGLONG>(1024 * 1024 * 1024);
|
||||||
*TotalNumberOfBytes = 9223372036854775807;
|
*TotalNumberOfBytes = 9223372036854775807;
|
||||||
*TotalNumberOfFreeBytes = 9223372036854775807;
|
*TotalNumberOfFreeBytes = 9223372036854775807;
|
||||||
|
|
||||||
@@ -724,7 +738,7 @@ private:
|
|||||||
UNREFERENCED_PARAMETER(dokanFileInfo);
|
UNREFERENCED_PARAMETER(dokanFileInfo);
|
||||||
|
|
||||||
wcscpy_s(VolumeNameBuffer, VolumeNameSize, L"SiaDrive");
|
wcscpy_s(VolumeNameBuffer, VolumeNameSize, L"SiaDrive");
|
||||||
*VolumeSerialNumber = 0x19831116;
|
*VolumeSerialNumber = 0x19831186;
|
||||||
*MaximumComponentLength = 256;
|
*MaximumComponentLength = 256;
|
||||||
*FileSystemFlags = FILE_CASE_SENSITIVE_SEARCH | FILE_CASE_PRESERVED_NAMES |
|
*FileSystemFlags = FILE_CASE_SENSITIVE_SEARCH | FILE_CASE_PRESERVED_NAMES |
|
||||||
FILE_SUPPORTS_REMOTE_STORAGE | FILE_UNICODE_ON_DISK |
|
FILE_SUPPORTS_REMOTE_STORAGE | FILE_UNICODE_ON_DISK |
|
||||||
@@ -971,7 +985,7 @@ private:
|
|||||||
// if open with FILE_FLAG_DELETE_ON_CLOSE
|
// if open with FILE_FLAG_DELETE_ON_CLOSE
|
||||||
if (dokanFileInfo->IsDirectory)
|
if (dokanFileInfo->IsDirectory)
|
||||||
{
|
{
|
||||||
if (filePath.RemoveDirectory())
|
if (RetryableAction(filePath.RemoveDirectory(), DEFAULT_RETRY_COUNT, DEFAULT_RETRY_DELAY_MS))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -980,7 +994,7 @@ private:
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (filePath.DeleteFile())
|
if (RetryableAction(filePath.RemoveDirectory(), DEFAULT_RETRY_COUNT, DEFAULT_RETRY_DELAY_MS))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1040,11 +1054,13 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
while ((ret == STATUS_SUCCESS) && (::FindNextFile(findHandle, &findData) != 0));
|
while ((ret == STATUS_SUCCESS) && (::FindNextFile(findHandle, &findData) != 0));
|
||||||
|
if (ret != STATUS_DIRECTORY_NOT_EMPTY)
|
||||||
DWORD error = GetLastError();
|
|
||||||
if (error != ERROR_NO_MORE_FILES)
|
|
||||||
{
|
{
|
||||||
ret = DokanNtStatusFromWin32(error);
|
DWORD error = GetLastError();
|
||||||
|
if (error != ERROR_NO_MORE_FILES)
|
||||||
|
{
|
||||||
|
ret = DokanNtStatusFromWin32(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::FindClose(findHandle);
|
::FindClose(findHandle);
|
||||||
@@ -1321,6 +1337,9 @@ public:
|
|||||||
_dokanOptions.ThreadCount = 0; // use default
|
_dokanOptions.ThreadCount = 0; // use default
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
_dokanOptions.Options = DOKAN_OPTION_DEBUG | DOKAN_OPTION_DEBUG_LOG_FILE;
|
_dokanOptions.Options = DOKAN_OPTION_DEBUG | DOKAN_OPTION_DEBUG_LOG_FILE;
|
||||||
|
_dokanOptions.Timeout = (60 * 1000) * 60;
|
||||||
|
#else
|
||||||
|
_dokanOptions.Options = 0;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user