Skip to content

Commit 27b0ae4

Browse files
SNOW-1545648: Fix PUT command error if file path contains spaces and single quotes (#1066)
Co-authored-by: Krzysztof Nozderko <[email protected]>
1 parent 580da21 commit 27b0ae4

File tree

4 files changed

+97
-24
lines changed

4 files changed

+97
-24
lines changed

Snowflake.Data.Tests/IntegrationTests/FileUploadDownloadLargeFilesIT.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,33 +23,33 @@ public class FileUploadDownloadLargeFilesIT : SFBaseTest
2323
private static readonly string s_fullFileName = Path.Combine(s_localFolderName, FileName);
2424
private static readonly string s_fullDownloadedFileName = Path.Combine(s_downloadFolderName, FileName);
2525
private static readonly MD5 s_md5 = MD5.Create();
26-
26+
2727
[OneTimeSetUp]
2828
public static void GenerateLargeFileForTests()
2929
{
3030
CreateLocalDirectory(s_localFolderName);
3131
GenerateLargeFile(s_fullFileName);
3232
}
33-
33+
3434
[OneTimeTearDown]
3535
public static void DeleteGeneratedLargeFile()
3636
{
3737
RemoveLocalFile(s_fullFileName);
3838
RemoveDirectory(s_localFolderName);
3939
}
40-
40+
4141
[Test]
4242
public void TestThatUploadsAndDownloadsTheSameFile()
4343
{
4444
// act
4545
UploadFile(s_fullFileName, s_remoteFolderName);
4646
DownloadFile(s_remoteFolderName, s_downloadFolderName, FileName);
47-
47+
4848
// assert
4949
Assert.AreEqual(
5050
CalcualteMD5(s_fullFileName),
5151
CalcualteMD5(s_fullDownloadedFileName));
52-
52+
5353
// cleanup
5454
RemoveFilesFromServer(s_remoteFolderName);
5555
RemoveLocalFile(s_fullDownloadedFileName);
@@ -85,7 +85,7 @@ private void DownloadFile(string remoteFolderName, string downloadFolderName, st
8585
command.ExecuteNonQuery();
8686
}
8787
}
88-
88+
8989
private void RemoveFilesFromServer(string remoteFolderName)
9090
{
9191
using (var conn = new SnowflakeDbConnection())
@@ -108,7 +108,7 @@ private static string CalcualteMD5(string fullFileName)
108108
}
109109

110110
private static void RemoveLocalFile(string fullFileName) => File.Delete(fullFileName);
111-
111+
112112
private static void CreateLocalDirectory(string path) => Directory.CreateDirectory(path);
113113

114114
private static void RemoveDirectory(string path) => Directory.Delete(path, true);

Snowflake.Data.Tests/IntegrationTests/SFPutGetTest.cs

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
33
*/
44

@@ -560,16 +560,36 @@ public void TestPutGetGcsDownscopedCredential(
560560
}
561561
}
562562

563+
[Test]
564+
public void TestPutGetFileWithSpaceAndSingleQuote(
565+
[Values] StageType stageType,
566+
[Values("/STAGE PATH WITH SPACE")] string stagePath)
567+
{
568+
PrepareTest(null, stageType, stagePath, false, true, true);
569+
using (var conn = new SnowflakeDbConnection(ConnectionString))
570+
{
571+
conn.Open();
572+
PutFile(conn, "", ResultStatus.UPLOADED, true);
573+
CopyIntoTable(conn, true);
574+
GetFile(conn, true);
575+
}
576+
}
577+
563578
private void PrepareTest(string sourceFileCompressionType, StageType stageType, string stagePath,
564-
bool autoCompress, bool clientEncryption = true)
579+
bool autoCompress, bool clientEncryption = true, bool makeFilePathWithSpace = false)
565580
{
566581
t_stageType = stageType;
567582
t_sourceCompressionType = sourceFileCompressionType;
568583
t_autoCompress = autoCompress;
569584
// Prepare temp file name with specified file extension
570585
t_fileName = Guid.NewGuid() + ".csv" +
571-
(t_autoCompress? SFFileCompressionTypes.LookUpByName(t_sourceCompressionType).FileExtension: "");
572-
t_inputFilePath = Path.GetTempPath() + t_fileName;
586+
(t_autoCompress ? SFFileCompressionTypes.LookUpByName(t_sourceCompressionType).FileExtension : "");
587+
var sourceFolderWithSpace = $"{Guid.NewGuid()} source file path with space";
588+
var inputPathBase = makeFilePathWithSpace ?
589+
Path.Combine(s_outputDirectory, sourceFolderWithSpace) :
590+
Path.GetTempPath();
591+
t_inputFilePath = Path.Combine(inputPathBase, t_fileName);
592+
573593
if (IsCompressedByTheDriver())
574594
{
575595
t_destCompressionType = "gzip";
@@ -580,7 +600,16 @@ private void PrepareTest(string sourceFileCompressionType, StageType stageType,
580600
t_destCompressionType = t_sourceCompressionType;
581601
t_outputFileName = t_fileName;
582602
}
583-
t_outputFilePath = $@"{s_outputDirectory}/{t_outputFileName}";
603+
var destinationFolderWithSpace = $"{Guid.NewGuid()} destination file path with space";
604+
var outputPathBase = makeFilePathWithSpace ?
605+
Path.Combine(s_outputDirectory, destinationFolderWithSpace) :
606+
s_outputDirectory;
607+
t_outputFilePath = Path.Combine(outputPathBase, t_outputFileName);
608+
if (makeFilePathWithSpace)
609+
{
610+
Directory.CreateDirectory(inputPathBase);
611+
Directory.CreateDirectory(outputPathBase);
612+
}
584613
t_filesToDelete.Add(t_outputFilePath);
585614
PrepareFileData(t_inputFilePath);
586615

@@ -610,16 +639,17 @@ private static bool IsCompressedByTheDriver()
610639
string PutFile(
611640
SnowflakeDbConnection conn,
612641
String additionalAttribute = "",
613-
ResultStatus expectedStatus = ResultStatus.UPLOADED)
642+
ResultStatus expectedStatus = ResultStatus.UPLOADED,
643+
bool encloseInSingleQuotes = false)
614644
{
615645
string queryId;
616646
using (var command = conn.CreateCommand())
617647
{
618648
// Prepare PUT query
619-
string putQuery =
620-
$"PUT file://{t_inputFilePath} {t_internalStagePath}" +
621-
$" AUTO_COMPRESS={(t_autoCompress ? "TRUE" : "FALSE")}" +
622-
$" {additionalAttribute}";
649+
var putQuery = encloseInSingleQuotes ?
650+
$"PUT 'file://{t_inputFilePath.Replace("\\", "/")}' '{t_internalStagePath}'" :
651+
$"PUT file://{t_inputFilePath} {t_internalStagePath}";
652+
putQuery += $" AUTO_COMPRESS={(t_autoCompress ? "TRUE" : "FALSE")}" + $" {additionalAttribute}";
623653
// Upload file
624654
command.CommandText = putQuery;
625655
var reader = command.ExecuteReader();
@@ -661,7 +691,7 @@ string PutFile(
661691
}
662692

663693
// COPY INTO - Copy data from the stage into temp table
664-
private void CopyIntoTable(SnowflakeDbConnection conn)
694+
private void CopyIntoTable(SnowflakeDbConnection conn, bool encloseInSingleQuotes = false)
665695
{
666696
using (var command = conn.CreateCommand())
667697
{
@@ -671,7 +701,8 @@ private void CopyIntoTable(SnowflakeDbConnection conn)
671701
command.CommandText = $"COPY INTO {t_schemaName}.{t_tableName}";
672702
break;
673703
default:
674-
command.CommandText =
704+
command.CommandText = encloseInSingleQuotes ?
705+
$"COPY INTO {t_schemaName}.{t_tableName} FROM '{t_internalStagePath}/{t_fileName}'" :
675706
$"COPY INTO {t_schemaName}.{t_tableName} FROM {t_internalStagePath}/{t_fileName}";
676707
break;
677708
}
@@ -696,12 +727,14 @@ private void CopyIntoTable(SnowflakeDbConnection conn)
696727
}
697728

698729
// GET - Download from the stage into local directory
699-
private void GetFile(DbConnection conn)
730+
private void GetFile(DbConnection conn, bool encloseInSingleQuotes = false)
700731
{
701732
using (var command = conn.CreateCommand())
702733
{
703734
// Prepare GET query
704-
var getQuery = $"GET {t_internalStagePath}/{t_fileName} file://{s_outputDirectory}";
735+
var getQuery = encloseInSingleQuotes ?
736+
$"GET '{t_internalStagePath}/{t_fileName}' 'file://{Path.GetDirectoryName(t_outputFilePath).Replace("\\", "/")}'" :
737+
$"GET {t_internalStagePath}/{t_fileName} file://{s_outputDirectory}";
705738

706739
// Download file
707740
command.CommandText = getQuery;

Snowflake.Data.Tests/UnitTests/SFFileTransferAgentTests.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
33
*/
44

@@ -69,6 +69,10 @@ class SFFileTransferAgentTest : SFBaseTest
6969
// Mock file content
7070
const string FileContent = "FTAFileContent";
7171

72+
// Mock file paths
73+
static readonly string s_filePathWithoutSpaces = Path.Combine("C:\\Users\\Test\\", "folder_without_space", "*.*");
74+
static readonly string s_filePathWithSpaces = Path.Combine("C:\\Users\\Test\\", "folder with space", "*.*");
75+
7276
[SetUp]
7377
public void BeforeEachTest()
7478
{
@@ -634,5 +638,35 @@ public void TestDownloadThrowsErrorDirectoryNotFound()
634638
Assert.IsInstanceOf<DirectoryNotFoundException>(innerException);
635639
Assert.That(innerException?.Message, Does.Match("Could not find a part of the path .*"));
636640
}
641+
642+
[Test]
643+
public void TestGetFilePathWithoutSpacesFromPutCommand()
644+
{
645+
TestGetFilePathFromPutCommand("PUT file://" + s_filePathWithoutSpaces + " @TestStage", s_filePathWithoutSpaces);
646+
}
647+
648+
[Test]
649+
public void TestGetFilePathWithSpacesFromPutCommand()
650+
{
651+
TestGetFilePathFromPutCommand("PUT file://" + s_filePathWithSpaces + " @TestStage", s_filePathWithSpaces);
652+
}
653+
654+
[Test]
655+
public void TestGetFilePathWithoutSpacesAndWithSingleQuotesFromPutCommand()
656+
{
657+
TestGetFilePathFromPutCommand("PUT 'file://" + s_filePathWithoutSpaces + "' @TestStage", s_filePathWithoutSpaces);
658+
}
659+
660+
[Test]
661+
public void TestGetFilePathWithSpacesAndWithSingleQuotesFromPutCommand()
662+
{
663+
TestGetFilePathFromPutCommand("PUT 'file://" + s_filePathWithSpaces + "' @TestStage", s_filePathWithSpaces);
664+
}
665+
666+
public void TestGetFilePathFromPutCommand(string query, string expectedFilePath)
667+
{
668+
var actualFilePath = SFFileTransferAgent.getFilePathFromPutCommand(query);
669+
Assert.AreEqual(expectedFilePath, actualFilePath);
670+
}
637671
}
638672
}

Snowflake.Data/Core/FileTransfer/SFFileTransferAgent.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,13 +527,19 @@ internal async Task updatePresignedUrlAsync(CancellationToken cancellationToken)
527527
/// </summary>
528528
/// <param name="query">The query containing the file path</param>
529529
/// <returns>The file path contained by the query</returns>
530-
private string getFilePathFromPutCommand(string query)
530+
internal static string getFilePathFromPutCommand(string query)
531531
{
532532
// Extract file path from PUT command:
533533
// E.g. "PUT file://C:<path-to-file> @DB.SCHEMA.%TABLE;"
534534
int startIndex = query.IndexOf("file://") + "file://".Length;
535535
int endIndex = query.Substring(startIndex).IndexOf('@') - 1;
536-
string filePath = query.Substring(startIndex, endIndex);
536+
string filePath = query.Substring(startIndex, endIndex).TrimEnd();
537+
538+
// Check if file path contains an enclosing (') char
539+
if (filePath[filePath.Length - 1] == '\'')
540+
{
541+
filePath = filePath.Substring(0, filePath.Length - 1);
542+
}
537543
return filePath;
538544
}
539545

0 commit comments

Comments
 (0)