我在数据库中有许多文件(PDF,doc,docx,jpg等),这些文件是使用以下方法添加的:
附件以MIME编码(base64编码)发送到数据库 字符串。然后该组件转换此MIME编码的字符串(in unicode)在将字节流作为BLOB写入数据库之前到字节流 (Oracle)或Image(SQL Server)。
还有一个' guid'附加到字符串的开头,即76个字符。
我试图将附件提取并保存到磁盘上的文件而不是数据库中。它的工作时间约占20%。剩余的时间我将字符串传递给System.FormatException: Invalid character in a Base-64 string.
时获得FromBase64String
。
我注意到数据库中的值如下所示,保存成功:
0x7B00350030003100460032003300350046002D00370
失败者总是这样开始:
0x7B35303146323335462D373546302D343936342D394
我在这里没有足够的字符来粘贴完整的示例,因此请参阅this pastebin以获取一个不起作用的示例。它应该代表一个Word文档,该文档仅显示"仅测试文档"。 This one是同一文档,但已转换为PDF。
This可以使用并转换为test font.htm
。它必须插入SQL数据库中的image
列,然后使用我的代码拉出:
private const int guidLength = 38 * 2;
public static byte[] GetAttachment(string folderid, string filename) {
string queryString = string.Format("SELECT <image column> FROM AttachmentTable WHERE .....",
folderid, filename);
using (SqlConnection connection = new SqlConnection("context connection=true"))
{
connection.Open();
using (SqlCommand selectAttachment = new SqlCommand(
queryString,
connection))
{
using (SqlDataReader reader = selectAttachment.ExecuteReader()) {
while (reader.Read())
{
if (reader[0] == System.DBNull.Value)
return new byte[0];
byte[] data = (byte[])reader[0];
byte[] truncatedData;
if (data[data.Length - 2] == 0)
truncatedData = new byte[data.Length - guidLength - 2];
else
truncatedData = new byte[data.Length - guidLength];
Array.Copy(data, guidLength, truncatedData, 0, truncatedData.Length);
// base64 unencode
string truncatedString = Encoding.Unicode.GetString(truncatedData);
return Convert.FromBase64String(truncatedString);
}
}
}
}
}
然后保存附件:
public static void SaveAttachmentToFile(string file, string folderid, string fileName)
{
byte[] data = GetAttachment(file, folderid);
if (data == null)
throw new ArgumentNullException("Attachment has no data, it may have been deleted");
using (FileStream writer = new FileStream(fileName, FileMode.Create))
{
writer.Write(data, 0, data.Length);
}
}
SQL CLR功能
[SqlFunction(IsDeterministic = true,
IsPrecise = true,
DataAccess = DataAccessKind.Read,
SystemDataAccess = SystemDataAccessKind.Read)]
public static SqlString WriteToFile(SqlString path, SqlString folderid, SqlString fileName)
{
try
{
if (!path.IsNull && !folderid.IsNull && !fileName.IsNull)
{
var dir = Path.GetDirectoryName(path.Value);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
string filename = Convert.ToString(fileName);
string folderid = Convert.ToString(efolderid);
string filepath = Convert.ToString(path);
SaveAttachmentToFile(filename, folderid, filepath);
return "Wrote file";
}
else
return "No data passed to method!";
}
catch (IOException e)
{
return "Make sure the assembly has external access!\n" + e.ToString();
}
catch (Exception ex)
{
return ex.ToString();
}
}
注意,上面的所有C#代码都被编译成一个程序集,然后用作CLR函数:
CREATE FUNCTION [dbo].[WriteToFile](@path [nvarchar](max), @efolderid [nvarchar](max), @filename [nvarchar](max))
RETURNS [nvarchar](max) WITH EXECUTE AS CALLER
AS
EXTERNAL NAME [ClassLibrary1].[CLR.UserDefinedFunctions].[WriteToFile]
GO
我认为我的问题可能与编码有关。我以为我可以使用Encoding.MIME.GetString
,但它不存在。我也试过了UTF-8
,但获得了0%的成功率。 Unicode似乎有效,但如上所述,成功率约为20%。
我的问题是,为什么其中一些无法保存(不正确的base64字符..但为什么?)但其他人工作正常?如何确定要使用的正确编码?有一种模式,但我真的不知道如何从这里开始。
答案 0 :(得分:1)
给定的数据插入方法不明确; “unicode”实际上不是文本编码;它是将符号表示为数字的一般系统。 .Net框架确实有一个名为“Unicode”的编码,但这是一个误称,这个编码实际上是UTF-16。
现在,您提到的数据有两种格式;一个有效,一个无效。这两种格式之间的区别在于其中一种格式在每个数据字节之间有00
个字节。这对应于UTF-16-LE,其中所有符号都是16位,也就是2个字节,其中最低部分存储在第一个字节中。没有那些00
字节的压缩数据应该是纯ASCII。
这种UTF-16格式实际上是一种非常愚蠢的保存Base64数据的方式,因为根据定义,Base64 总是纯7位ascii;永远不会使用这些额外的字节,只需将保存该数据所需的空间加倍。事实上,当保存为字节时,Base64编码也没有任何优点,因为Base64的目的是将二进制数据转换为纯文本,因此它可以由无法处理存储/传输二进制数据的系统处理。鉴于此Base64文本随后在数据库中保存为二进制LOB,这显然不是这种情况。
除此之外,00
字节做在这里为您的问题提供解决方案:正如我所说,对于Base64内容,这些中间字节将永远不会被使用,这意味着它们将始终为00
。另一方面,Base64始终是纯ASCII文本,从不包含00字节。这意味着您可以检查那些00
字节并使用它们的存在来选择正确的编码。
请注意,在将字节转换为字符串之后切断GUID 要简单得多,因为它将始终具有38的长度,而不是ASCII中的38字节或UTF-16中的76个字节。
将第一个代码块的阅读器部分改为此应解决问题:
using (SqlDataReader reader = selectAttachment.ExecuteReader())
{
// only reading one anyway; doesn't need to be a 'while'.
if (!reader.Read())
return new byte[0];
if (reader[0] == System.DBNull.Value)
return new byte[0];
byte[] data = (byte[])reader[0];
if (data.Length == 0)
return new byte[0];
String base64String
if (data.Length > 1 && data[1] == 00)
base64String = Encoding.Unicode.GetString(data);
else
base64String = Encoding.ASCII.GetString(data);
// Cuts off the GUID, and takes care of any trailing 00 bytes.
String truncatedString = base64String.Substring(38).TrimEnd('\0');
return Convert.FromBase64String(truncatedString);
}