Как перелOжить файловую папку в базу

Операция, известная в миру как Mounting a File System on a Volume, должна заключаться в создании базы данных SQL Server на сырую партицию. Этого достаточно, потому что лучшей файловой системой является SQL Server. Так же, как лучший девайс всех времен и народов для превращения стека в очередь, это автомат Калашникова. В SQL Server, по крайней мере, со времен 6.5 (апрель 1996 г.), а, скорее всего, и раньше, существовала возможность создания базы данных на raw partition. Хотя последний раз об этом явно писалось в 2000-м (https://msdn.microsoft.com/en-us/library/aa933078(SQL.80).aspx), данная возможность никуда не делась и о сю пору благополучно присутствует в версии 2008, откуда плавно перейдет в 2008 R2, он же 2010, он же Килиманджаро. Откройте в BOL топег https://msdn.microsoft.com/ru-ru/library/ms176061.aspx. Помимо русского перевода, который, как всегда, прошибает слезу, примечательным моментом в нем является это место: "Если файл находится в необработанной секции, аргумент os_file_name должен указывать только букву диска существующей необработанной секции. В каждой необработанной секции может быть создан только один файл". Под необработанной секцией, если говорить на понятном языке, здесь подразумевается неотформатированный раздел диска. По хорошему, файловая система нужна только для того, чтобы поддерживать файлы баз данных SQL Server, предоставляя им сервисы вроде compression, encryption, differential backup, sparse files support для database snapshots, alternate streams, которые в некоторых случаях использует dbcc, может, еще что по мелочи. Да и то, например, FAT32 этого не умеет, только NTFS. А, еще RsFx0102 и filestream. Видите, даже WinFS - это, по большому счету, SQL Server J. Представив файлы в виде записей таблицы, мы получаем возможность их централизованного хранения и резервного копирования с сохранением всех базовых операций над файлами средствами обычного T-SQL, включая поиск и полнотекстовый поиск. В связи с этим йа нопесал оч.хор. хр.пр., к-я перекладывает целиком заданную папку в SQL Server со всем содержимым ее файлов.

 

На самом деле их тут две с половиной штуки. Первая (InitMethod + FillRow + fnRecursive) – это TVF (table-valued function, функция, возвращающая результат в табличном виде), принимающая на входе полный путь к файловой папке, и возвращающая в данном случае рекордсет следующей структуры:

ID HierarchyID, FullName nvarchar(1000), DateModified datetime2, DateCreated datetime2, LastAccessed datetime2, size bigint, isDir bit, Properties xml

 

Первое поле используется в качестве идентификатора записи в таблице. Заодно оно определяет положение узла в иерархии, что позволяет при построении в таблице дерева обойтись без традиционной связки parent-child. См. на эту тему посты HierarchyID и parent-child, Dir() и HierarchyID.

Следующие поля – это традиционные атрибуты файла: его полное имя (с путем), дата последнего изменения, дата создания и дата последнего доступа, а также размер (в байтах) и булевый признак того, является ли данный объект фолдером или файлом.

Поле Properties содержит расширенные атрибуты файла, про которые знает Windows Shell. См. на эту тему пост Расширенные свойства файлов. Это атрибуты типа Album, Artist, Bit rate, Date taken, Width, Height, Focal length и др. Поскольку у одних типов файлов они есть, у других нет, зато у них есть те, которых нет у этих, я сначала хотел использовать модную фишку 2008-го под названием sparse columns, но потом решил, что это будет отдельная тема и сложил их всех в XML. Как управляться с SQL Serverным XML из приложения, можно прочитать в посте Возвращение SqlXml. Вспомогательная функция FileExtProps собирает расширенные атрибуты файла, которые возвращает в виде SqlXml.

Устройство CLRной TVF разбиралось в посте Табличные CLR-функции для ТЧайников. Из него можно почерпнуть, для чего нужны якорный метод InitMethod, итерационный метод FillRow, а также структурный тип row_item и список из элементов этого типа. В случае глубокой навигации по фолдеру, требуется обшаривать вложенные подфолдеры, рекурсивно вызывая для них те же действия, что и для родительского фолдера. В принципе, рекурсивная TVF ничем идейно не отличается от обычной, но на всякий случай можно прочитать посты Рекурсивные CLR TVF и Рекурсивные TVF-2. В качестве вспомогательной функции при организации рекурсии выступает fnRecursive.

По мере того, как перебираются файлы, хотелось бы выводить какую-либо информацию о прогрессе процесса. TVF не может выводить ничего статусного, кроме своего результирующего рекордсета, поэтому я решил создать некоторое подобие журнала, куда записывать текущий файл и время. Было бы логично держать его вместе с остальными журналами сообщений SQL Server. Функция GetSqlErrLogPath() получает фолдер, в котором SQL Server держит свои Error Logи. Местоположение служебных объектов SQL Server разбиралось в посте Как определить дефолтную локацию для файлов БД. Поскольку эта функция обращается к реджистри, то методу InitMethod, из которого она вызывается, ставится атрибут SystemDataAccessKind.Read.

Рекурсивная процедура fnRecursive формирует новую запись типа row_item в списке для каждого перебраного файла или подфолдера, наполняя ее поля. HierarchyId получается от родительского значения и ближайшего левого соседа. Расширенные атрибуты собираются функцией FileExtProps в виде XML, который также вставляется в row_item. Прочие атрибуты берутся из объекта IO.FileSystemInfo, представляющем собой обобщение файла и директории. Если это файл, для него еще берется размер, если директория, процедура fnRecursive вызывает сама себя с параметром этой директории.

Сборка оформляется в виде библиотеки классов и деплоится на SQL Server вручную, поскольку в ней используется СОМовская библиотека %windir%\System32\SHELL32.dll. Про это можно прочитать в посте Использование COMовских dll в SQL CLR. По этой же причине сборка должна деплоиться как unsafe – см. Подписание внешней или небезопасной сборки. А сопло мы ей прикрутим деревянненькое, потому что до этого места все равно никто не дочитает. Согласно последним отчетам Росстат удаление mssqlsystemresource.mdf позволяет поднять производительность SQL Server на 12.3%. Деплоймент скомпилированой сборки выполняется средствами Т-SQL:

use TestFS

--select * from sys.objects

if object_id('Dir', 'FT') is not null drop function Dir

go

if object_id('GetSqlErrLogPath', 'FS') is not null drop function GetSqlErrLogPath

go

if object_id('LoadDir', 'PC') is not null drop proc LoadDir

go

if exists(select 1 from sys.assemblies where name = 'MyAssembly') drop assembly MyAssembly

go

create assembly MyAssembly from 'C:\Temp\ClassLibrary1\bin\Debug\ClassLibrary1.dll' with permission_set = unsafe

go

Скрипт 1

В принципе, функция GetSqlErrLogPath() играет вспомогательную роль во всем этом хозяйстве, но я подумал, что раз от нее никакого вреда, пусть будет доступна как SQLная скалярная функция и сделал ее public и снабдил атрибутом SqlFunction, что позволяет вызывать ее самостоятельно из SQL Server:

create function GetSqlErrLogPath() returns nvarchar(500) as external name MyAssembly.FileSystem.GetSqlErrLogPath

go

select dbo.GetSqlErrLogPath()

-------------------------------------------------------------------

C:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\Log

Скрипт 2

Функция Dir(), получающая представление папки в табличном виде, идейно повторяет содержание поста Синхронизация файловых каталогов средствами SQL Server.

create function Dir (@filename nvarchar(255), @shallowTraversal bit)

returns table (ID HierarchyID, FullName nvarchar(1000), DateModified datetime2, DateCreated datetime2, LastAccessed datetime2, Properties xml, size bigint, isDir bit)

as external name MyAssembly.FileSystem.InitMethod

go

if object_id('t', 'U') is not null drop table t

select * into t from Dir('c:\Temp', 0)

select * from t

Скрипт 3

image001

рис.1

Дополнять ее вытаскиванием файлового контента я не стал, опасаясь переполнения памяти (см. пост Импорт/экспорт блобов в файлы). После того, как результат функции будет положен в таблицу, таблицу можно дополнить полем varbinary(max) и на основе полного имени файла в каждой записи вытащить в нее содержание этого файла. В идеале это можно было сделать влет одной строчкой: update t set content = (select BulkColumn from openrowset(bulk FullName, single_blob) as f), если бы дурацкая функция openrowset понимала в качестве имени файла выражения. Но она хочет строковую константу, поэтому придется помучиться.

declare @fullName nvarchar(500), @cmd nvarchar(1000), @i int = 0

set nocount on

declare cur cursor forward_only scroll_locks for select fullName from t where isDir = 0 for update of content; open cur

while 1 = 1 begin

 fetch next from cur into @fullName

 if @@fetch_status <> 0 break

 set @i += 1

 print cast(@i as varchar(10)) + '. ' + @fullName

 set @cmd = 'update t set content = (select BulkColumn from openrowset(bulk ''' + @fullName + ''', single_blob) as f) where current of cur'

 begin try; exec (@cmd); end try

 begin catch; print ' Не могу загрузить файл по причине ' + Error_Message(); end catch

end

close cur; deallocate cur

set nocount off

Скрипт 4

 

Я понимаю, что ни курсоры, ни динамический SQL не способствуют оптимальности кода, я и сам про это регулярно рассказываю слушателям с умным видом. Однако бывают ситуации, когда этого не избежать. Жизнь есть жизнь, и практические задачи, которые в ней возникают, не всегда укладываются под гладкую канву учебника. Здесь еще создатели T-SQL постарались, обложив со всех сторон ограничениями. Сомнительно, чтобы константная строка в функциях openquery/openrowset была настолько низкоуровневым архитектурным ограничением, чтобы тащить его из версии в версию, заставляя разработчиков настолько через одно место всякий раз его обходить. Непонятно, почему до сих пор не принимаются выражения в качестве параметров хранимых процедур, почему цикл for до сих пор приходится делать через while, вообще, почему T-SQL остается на уровне каменного века, хотя на дворе уже скоро 2010 год. В данном конкретном случае это принципиальной роли, к счастью, не играет: накладные расходы на курсор и пр. – копейки по сравнению с тем, сколько занимает перекачка в базу здорового файла. 450 мегов в моем случае перекачивались 2 мин. 36 сек. Залоченые файлы, которые она не смогла качнуть, я не учитывал. Все равно, это она что-то очень ударно сейчас постаралась. Пока отлаживался, разброс времен был в диапазоне 3.5 – 4 мин. Было бы интересно ради хохмы засосать в базу весь диск с:, но поскольку он у меня занят далеко за половину, я решил повременить с этим экспериментом.

image003

рис.2

image005

рис.3

Я подготовил еще одну разновидность закачки в виде процедуры LoadDirWithFileContent. Она отличается от функции InitMethod тем, что отгружает фолдер напрямую в таблицу, имя которой передается ей в качестве одного из параметров, следовательно, блобы, которые мы аплоудим на SQL Server, сразу персистятся и их можно заливать тут же без боязни переполнить память. Таблица, в которую происходит загрузка, по условию, должна иметь обязательный набор полей с оговоренными именами и типами (см. таблицу Dir в Скрипте 5). Дополнительно к ним в таблице могут существовать другие поля. При заливке блобов внутри CLRной процедуры используется решение, рассмотренное в посте Импорт/экспорт блобовских полей в файлы – CLR. Если кто-либо, слышавший про SqlFileStream API, удивится, почему здесь не используется этот подход, почитайте пост Частичное обновление FILESTREAM. Служебная процедура spRecursive играет для хранимой процедуры LoadDir такую же роль, как fnRecursive для TVF Dir(). Функция Dir() и процедура LoadDir могут использоваться независимо, и у меня была идея оформить их в виде методов некоторого UDT, который просто рассматривать как контейнер для библиотеки функций. К сожалению, TVF не допускаются в виде методов UDT, поэтому все будет лежать россыпью. Создание процедуры LoadDir из сборки и ее запуск выглядят так:

drop function Dir

drop table t

go

create proc LoadDir @folder nvarchar(255), @shallowTraversal bit, @tblName sysname as external name MyAssembly.FileSystem.LoadDirWithFileContent

go

if object_id('Dir', 'U') is not null drop table Dir

create table Dir(ID HierarchyID, FullName nvarchar(1000), DateModified datetime2, DateCreated datetime2, LastAccessed datetime2, Properties xml, size bigint, isDir bit, Content varbinary(max))

exec LoadDir 'c:\Temp', 0, 'Dir' --0, если ныряем в подфолдеры; 1, если плавать мелко

Скрипт 5

Это загрузка ровно той же с:\Temp объемом 450 МБ в другую таблицу. Выполнение длилось 4 мин. 30 сек., что нетипично. Пока я отлаживался, типичные времена для нее были 3 мин. +/- пара секунд. Кажется, дело в том, что я не сделал drop table t. Обычно удалял таблицу t с результатами функции, прежде чем переходить к процедуре, а сейчас получился задвоеный размер базы – не 450 метров, а 936. Она стала автоприращать mdfник, а поскольку он лежит там же в c:\temp, они с чтением стали мешаться друг другу.

В ходе загрузки создается журнал. Он лежит в той же папке, что и ErrorLog, и называется жестко SqlFSLoader.log. Поскольку Flush() делается после каждой готовой строчки, можно переоткрывать его и наблюдать за протеканием процесса загрузки.

image007

рис.4

Приложение. CLR-исходник.

 

using System;

using System.Data;

using System.Data.SqlClient;

using System.Data.SqlTypes;

using Microsoft.SqlServer.Server;

using System.IO;

using Microsoft.SqlServer.Types;

using System.Collections;

using System.Collections.Generic;

using System.Xml;

using System.Text;

public partial class FileSystem

{

struct row_item

{

public SqlHierarchyId ID;

public string fullName;

public DateTime dateModified, dateCreated, lastAccessed;

public long size;

public SqlXml extProps;

public bool isDir;

}

static Shell32.ShellClass shell = new Shell32.ShellClass();

static TextWriter log;

static readonly string logName = "SqlFSLoader.log";

static SqlConnection cnn;

/// <summary>

/// Якорный метод CLRной TVF, выводящей содержимое файловой папки

/// </summary>

/// <param name="folder">Полный путь к папке</param>

/// <param name="shallowTraversal">Глубина погружения: true - только файлы и фолдеры данной папки, false - лезем вглубь

/// фолдеров до упора</param>

/// <returns>Содержимое в виде списка стр-р row_item</returns>

[SqlFunction(Name = "Dir", SystemDataAccess=SystemDataAccessKind.Read, FillRowMethodName = "FillRow",

TableDefinition = "ID HierarchyID, FullName nvarchar(1000), DateModified datetime2, DateCreated datetime2, LastAccessed datetime2, Properties xml, size bigint, isDir bit")]

public static IEnumerable InitMethod(string folder, bool shallowTraversal)

{

List<row_item> enumResult = new List<row_item>();

log = new StreamWriter(Path.Combine(GetSqlErrLogPath(), logName));

log.WriteLine("Журнал загрузки файлов в базу. Начало работы = " + DateTime.Now.ToString() + ".");

log.WriteLine("Функция Dir возвращает содержимое фолдера {0}{1}.", folder, shallowTraversal ? "" : " и подпапок");

log.WriteLine(new string('-', 80));

fnRecursive(folder, shallowTraversal, enumResult, SqlHierarchyId.GetRoot());

log.WriteLine(new string('-', 80));

log.WriteLine("Окончание работы = " + DateTime.Now.ToString());

log.Close();

return enumResult;

}

/// <summary>

/// Служебная процедура для InitMethod, собственно и выполняющая всю работу

/// </summary>

/// <param name="folder">Берется от InitMethod</param>

/// <param name="shallowTraversal">Берется от InitMethod</param>

/// <param name="enumResult">Будущий результат InitMethod, наполняемый в процессе рекурсивных вызовов</param>

/// <param name="parentId">HierarchyId текущей родительской папки, с которой погрузились в Recursive</param>

private static void fnRecursive(string folderPath, bool shallowTraversal, List<row_item> enumResult, SqlHierarchyId parentId)

{

FileSystemInfo[] fsi;

try //На случай, если к папке нет доступа

{

fsi = new DirectoryInfo(folderPath).GetFileSystemInfos();

}

catch (Exception e)

{

log.WriteLine(e.Message); return;

}

SqlHierarchyId leftSibling = SqlHierarchyId.Null;

Shell32.Folder shellFolder = shell.NameSpace(folderPath);

foreach (FileSystemInfo f in fsi)

{

row_item r = new row_item();

//Новый HierarchyId порождается на основе трех HierarchyId: родителя и левого и правого братьев.

//Правый брат у нас всегда будет null, т.к., спустившись на уровень, двигаемся слева направо.

//Левый брат = null в момент прихода на уровень, а дальше это будет предыдущий по циклу.

//Родительский HierarchyId - это параметр parentId.

r.ID = leftSibling = parentId.GetDescendant(leftSibling, SqlHierarchyId.Null);

r.fullName = f.FullName; r.dateModified = f.LastWriteTimeUtc; r.dateCreated = f.CreationTimeUtc; r.lastAccessed = f.LastAccessTimeUtc;

r.isDir = (f is DirectoryInfo);

r.extProps = FileExtProps(f.FullName, shellFolder);

if (!r.isDir) r.size = ((FileInfo)f).Length;

else if (! shallowTraversal) fnRecursive(f.FullName, false, enumResult, r.ID);

log.WriteLine(new string(' ', r.ID.GetLevel().Value) + r.fullName + " : " + DateTime.Now.ToString()); log.Flush();

enumResult.Add(r);

}

}

public static void FillRow(Object o, out SqlHierarchyId ID, out SqlString Name, out DateTime? dateModified, out DateTime? dateCreated, out DateTime? lastAccessed, out SqlXml extProps, out SqlInt64 size, out SqlBoolean isDir)

{

row_item r = (row_item)o;

ID = r.ID; Name = r.fullName; dateModified = r.dateModified; dateCreated = r.dateCreated; lastAccessed = r.lastAccessed; size = r.size; extProps = r.extProps; isDir = r.isDir;

}

/// <summary>

/// Загружает в таблицу фолдер вместе с содержимым его файлов.

/// </summary>

/// <param name="folder">Полный путь к фолдеру</param>

/// <param name="shallowTraversal">Лезем ли в поддиректории?</param>

/// <param name="tableName">Таблица, в которую загружаем. Таблица должна иметь в своей структуре поля, оговоренные в

/// атрибуте TableDefinition метода InitMethod:

/// ID HierarchyID, FullName nvarchar(1000), DateModified datetime2, DateCreated datetime2, LastAccessed datetime2, Properties xml, size bigint, isDir bit

/// плюс Content varbinary(max)/

/// Другие поля у нее возможны.</param>

[SqlProcedure(Name = "LoadDir")]

public static void LoadDirWithFileContent(string folder, bool shallowTraversal, string tableName)

{

cnn = new SqlConnection("context connection=true"); cnn.Open();

log = new StreamWriter(Path.Combine(GetSqlErrLogPath(), logName));

log.WriteLine("Журнал загрузки файлов в базу. Начало работы = " + DateTime.Now.ToString() + ".");

log.WriteLine("Процедура LoadDir качает содержимое фолдера {0}{1} в таблицу {2}.", folder, shallowTraversal ? "" : " и подпапок", tableName);

log.WriteLine(new string('-', 80));

//Проверка, что таблица с именем tableName существует в текущей базе

SqlCommand cmd = new SqlCommand("select count(1) from sys.tables where name = @tblName", cnn);

cmd.Parameters.Add(new SqlParameter("@tblName", tableName));

if ((int)cmd.ExecuteScalar() == 0)

{

cnn.Close(); //Поскольку Pipe.ExecuteAndSend(cmd) открывает свое контекстное соединение, предыдущее к этому моменту нужно закрыть.

cmd = new SqlCommand(String.Format("raiserror('Таблица с именем {0} не найдена в текущей базе {1}.', 16, 1)", tableName, cnn.Database));

try { SqlContext.Pipe.ExecuteAndSend(cmd); } //raiserror поднимает еще .NETовскую ошибку Msg 6522, Level 16, State 1 ...

catch (SqlException) { }; //которая со всем стеком вызовов будет добавлена к пользовательской, если ее вовремя не перехватить.

return;

}

spRecursive(folder, shallowTraversal, tableName, SqlHierarchyId.GetRoot());

log.WriteLine();

log.WriteLine(new string('-', 80));

log.Close();

cnn.Close();

}

private static void spRecursive(string folderPath, bool shallowTraversal, string tableName, SqlHierarchyId parentId)

{

FileSystemInfo[] fsi;

try //На случай, если к папке нет доступа

{

fsi = new DirectoryInfo(folderPath).GetFileSystemInfos();

}

catch (Exception e)

{

log.WriteLine(); log.Write(e.Message); return;

}

SqlCommand cmd = cnn.CreateCommand();

cmd.CommandText = "insert " + tableName + "(ID, FullName, DateModified, DateCreated, LastAccessed, Properties, size, isDir, Content) values (@hid, @fullName, @dateModified, @dateCreated, @lastAccessed, @properties, @size, @isDir, @content)";

cmd.Parameters.Add(new SqlParameter("@hid", SqlHierarchyId.Null)); //SqlDbType.SqlHierarchyId в списке нет, приходится обойтись значением.

cmd.Parameters["@hid"].UdtTypeName = "HierarchyID"; //Иначе System.ArgumentException: UdtTypeName property must be set for UDT parameters.

cmd.Parameters.Add(new SqlParameter("@fullName", SqlDbType.NVarChar));

cmd.Parameters.Add(new SqlParameter("@dateModified", SqlDbType.DateTime2));

cmd.Parameters.Add(new SqlParameter("@dateCreated", SqlDbType.DateTime2));

cmd.Parameters.Add(new SqlParameter("@lastAccessed", SqlDbType.DateTime2));

cmd.Parameters.Add(new SqlParameter("@properties", SqlDbType.Xml));

cmd.Parameters.Add(new SqlParameter("@size", SqlDbType.BigInt));

cmd.Parameters.Add(new SqlParameter("@isDir", SqlDbType.Bit));

cmd.Parameters.Add(new SqlParameter("@content", SqlDbType.VarBinary));

SqlHierarchyId leftSibling = SqlHierarchyId.Null;

Shell32.Folder shellFolder = shell.NameSpace(folderPath);

FileStream fs = null;

foreach (FileSystemInfo fd in fsi)

{

cmd.Parameters["@hid"].Value = leftSibling = parentId.GetDescendant(leftSibling, SqlHierarchyId.Null);

cmd.Parameters["@fullName"].Value = fd.FullName;

cmd.Parameters["@properties"].Value = FileExtProps(fd.FullName,shellFolder);

cmd.Parameters["@isDir"].Value = (fd is DirectoryInfo);

cmd.Parameters["@dateModified"].Value = fd.LastWriteTimeUtc;

cmd.Parameters["@dateCreated"].Value = fd.CreationTimeUtc;

cmd.Parameters["@lastAccessed"].Value = fd.LastAccessTimeUtc;

try //На случай, если к файлу нет доступа, OpenRead() вылетит в исключение

{

log.WriteLine();

string spaces = new string(' ', leftSibling.GetLevel().Value);

log.Write(spaces + fd.FullName + " : " + DateTime.Now.ToString() + "... "); log.Flush();

if (fd is FileInfo)

{

cmd.Parameters["@size"].Value = ((FileInfo)fd).Length;

fs = ((FileInfo)fd).OpenRead();

cmd.Parameters["@content"].Value = new SqlBytes(fs);

}

else if (! shallowTraversal) spRecursive(fd.FullName, false, tableName, leftSibling);

cmd.ExecuteNonQuery();

if (fs != null)

{

log.Write("OK. ({0:### ### ### ### ##0.00} MБ)", fs.Length / 1024 / 1024.0); fs.Close(); fs = null;

}

else log.Write("\r\n" + spaces + "OK.");

}

catch (Exception e)

{

log.WriteLine(); log.Write(e.Message);

}

}

}

/// <summary>

/// Функция возвращает набор значений расширенных атрибутов заданного файла в виде XML.

/// Поскольку ожидается, что это не будет какой-нибудь здоровый XML, он не персистится,

/// а просто возвращается как результат ф-ции (SqlXml).

/// </summary>

/// <param name="fileFullName">Полное имя файла</param>

/// <returns></returns>

private static SqlXml FileExtProps(string fileFullName, Shell32.Folder shellFolder)

{

MemoryStream ms = new MemoryStream();

XmlTextWriter xtw = new XmlTextWriter(ms, Encoding.Unicode);

xtw.Formatting = Formatting.Indented;

xtw.WriteStartDocument();

xtw.WriteStartElement("Props");

xtw.WriteWhitespace("\n");

Shell32.FolderItem fi = shellFolder.Items().Item(Path.GetFileName(fileFullName));

//Собираем свойствa, все, сколько ни есть

for (int i = 0; ; i++)

{

string s = shellFolder.GetDetailsOf(null, i);

if (string.IsNullOrEmpty(s)) break;

xtw.WriteStartElement("Prop");

xtw.WriteAttributeString("id", i.ToString());

xtw.WriteAttributeString("Name", s);

xtw.WriteValue(shellFolder.GetDetailsOf(fi, i));

xtw.WriteEndElement();

xtw.WriteWhitespace("\n");

}

xtw.WriteEndElement();

xtw.WriteEndDocument();

xtw.Flush();

return new SqlXml(ms);

}

/// <summary>

/// Возвращает фолдер, в котором хранятся Error Logи текущего экземпляра SQL Server

/// </summary>

/// <returns></returns>

[SqlFunction(DataAccess=DataAccessKind.Read)]

public static string GetSqlErrLogPath()

{

bool noConnectionBefore = (cnn == null || cnn.State == ConnectionState.Closed);

//Данная функция используется из TVF (InitMethod) и sp (LoadDirWithFileContent).

//В первом случае соединения нет, во втором уже есть открытое. Для этого if.

if (noConnectionBefore)

{

cnn = new SqlConnection("context connection=true"); cnn.Open();

}

//Получаем имя текущего экземпляра

SqlCommand cmd = new SqlCommand("select isnull(cast(serverproperty('InstanceName') as nvarchar(500)), 'MSSQLSERVER')", cnn);

string instanceName = (string) cmd.ExecuteScalar();

if (noConnectionBefore) cnn.Close();

string instanceRegKey = (string) Microsoft.Win32.Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL", instanceName, "");

string sqlParmsArg1 = (string)Microsoft.Win32.Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\" + instanceRegKey + "\\MSSQLServer\\Parameters", "SQLArg1", "");

return Path.GetDirectoryName(sqlParmsArg1.Substring(2));

}

};

Скрипт 6