Синхронизация файловых каталогов средствами SQL Server
В третьей серии нашей программы я предлагаю расширить и углУбить бизнес-смысл демонстрационного примера. Мы написали CLR TVF, которая позволяет получать parent-child таблицу с содержимым заданного каталога файловой системы (https://blogs.msdn.com/alexejs/archive/2009/05/12/clr.aspx). Также мы научились ее подписывать и деплоить на SQL Server (https://blogs.msdn.com/alexejs/archive/2009/05/11/0-9-8-7-6-5-5-6.aspx). Достаточно совершить совсем немного элементарных действий, чтобы с ее помощью решить практическую задачу синхронизации двух папок. Под синхронизацией будем понимать в данном случае merge-сценарий. Предположим, имеются два диска: текущий рабочий и архивный. Архивный повторяет структуру рабочего диска, однако на нем ничего не удаляется, как и полагается при работе с хранилищем. На него просто с некоторой периодичностью сливаются обновления. Под обновлениями будем полагать ситуации, когда папка или файл на рабочем диске отсутствуют в архиве, далее, если дата последнего обновления файла на рабочем диске больше даты последнего обновления соответствующего файла в архиве и, наконец, если размеры этих файлов отличаются. Под соответствием файлов или подкаталогов будем понимать совпадение их относительных путей. Относительный путь - это все, что идет после папки источника или назначения, передаваемых в качестве параметра. Т.е. если мы синхронизируем папку c:\Temp в папку f:\Temp1, то относительный путь у файла c:\Temp\Folder\File.ext будет Folder\File.ext, а у файла f:\Temp1\Folder\File.ext - Folder\File.ext. Они совпадают, поэтому файл c:\Temp\Folder\File.ext будет соответствовать файлу f:\Temp1\Folder\File.ext. Если файла f:\Temp1\Folder\File.ext нет или его дата модификации меньше, чем у c:\Temp\Folder\File.ext или их размеры различны, файл c:\Temp\Folder\File.ext копируется в f:\Temp1, переписывая f:\Temp1\Folder\File.ext, если таковой уже существует.
У нас имеется замечательная функция dbo.Dir() (см. «Табличные CLR-функции для ТЧайников»), которая позволяет получить содержимое c:\Temp в виде таблицы. И содержимое f:\Temp1 в виде аналогичной таблицы. Нужно всего навсего их сджойнить левым образом, чтобы определить, какие файлы/подкаталоги источника c:\Temp требуется скопировать в назначение f:\Temp1. Ну и дополнительно к функции Dir() я дописал еще две процедуры: создания каталога и копирования файла. Они элементарны. Вот, что получилось в результате.
///CLRная библиотека для SQLной задачи слияния файловых папок.
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.IO;
using System.Collections;
using System.Collections.Generic;
public partial class UserDefinedFunctions
{
/// <summary>
/// Псевдозапись
/// </summary>
struct row_item
{
public string fullName;
public DateTime dateModified;
public long size;
public bool isDir;
}
/// <summary>
/// Якорный метод TVF. Выводит содержание файлов и подкаталогов заданного каталога в табличном виде.
/// TableDefinition - структура рекордсета, выводимого TVF.
/// Из атрибутов выводятся полное имя, дата посл.изм-я, размер, признак "каталог это или файл", родительский каталог.
/// Родительский каталог в псевдозаписи не храним, считаем при выводе.
/// </summary>
/// <param name="folder">Папка, dir которой выводим</param>
/// <param name="shallowTraversal">Сканируем только folder, или лезем в подфолдеры до упора?</param>
/// <returns>IEnumerable коллекция (в дан.случае List) псевдозаписей.</returns>
[Microsoft.SqlServer.Server.SqlFunction(Name = "Dir", FillRowMethodName = "FillRow",
TableDefinition = "fullName nvarchar(1000), dateModified datetime2, size bigint, isDir bit, parent nvarchar(1000)")]
public static IEnumerable InitMethod(string folder, bool shallowTraversal)
{
List<row_item> enumResult = new List<row_item>();
//Собираем в коллекцию файлы
foreach (string fileName in Directory.GetFiles(folder, "*", shallowTraversal ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories))
{
FileInfo fi = new FileInfo(fileName);
row_item r = new row_item(); r.fullName = fileName; r.dateModified = fi.LastWriteTimeUtc; r.size = fi.Length; r.isDir = false;
enumResult.Add(r);
}
//Затем каталоги
foreach (string dirName in Directory.GetDirectories(folder, "*", shallowTraversal ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories))
{
DirectoryInfo di = new DirectoryInfo(dirName);
row_item r = new row_item(); r.fullName = dirName; r.dateModified = di.LastWriteTime; r.isDir = true;
enumResult.Add(r);
}
return enumResult;
}
/// <summary>
/// В отл-е от Т-SQLной TVF CLRная гонит поток вместо законченного снимка. Данный метод вызывается, когда в потоке сдвигаемся
/// на след.псевдозапись.
/// </summary>
/// <param name="o">Очередная псевдозапись</param>
/// Далее идет список выходных п-ров, соотв-х стр-ре рекордсета, объявленной в атрибуте TableDefinition метода InitMethod.
/// <param name="fullName"></param>
/// <param name="dateModified"></param>
/// <param name="size"></param>
/// <param name="isDir"></param>
/// <param name="parent"></param>
public static void FillRow(Object o, out SqlString fullName, out DateTime? dateModified, out SqlInt64 size, out SqlBoolean isDir, out SqlString parent)
{
row_item r = (row_item)o;
fullName = r.fullName; dateModified = r.dateModified; size = r.size; isDir = r.isDir; parent = Path.GetDirectoryName(r.fullName);
}
}
public partial class StoredProcedures
{
/// <summary>
/// Если директория не существует, она создается.
/// </summary>
/// <param name="destFullName"></param>
[Microsoft.SqlServer.Server.SqlProcedure]
public static void FolderCreate(string destFullName)
{
if (!Directory.Exists(destFullName)) Directory.CreateDirectory(destFullName);
}
/// <summary>
/// Процедура копирует файл, переписывая назначение, если есть.
/// Если родительский фолдер назначения не существовал, он предварительно создается.
/// </summary>
/// <param name="sourceFullName">Полное имя, кого копируем.</param>
/// <param name="destFullName">Полное имя, куда копируем.</param>
[Microsoft.SqlServer.Server.SqlProcedure]
public static void FileCopy(string sourceFullName, string destFullName)
{
FolderCreate(Path.GetDirectoryName(destFullName));
File.Copy(sourceFullName, destFullName, true);
}
}
Скрипт 1
Подпишем проект, как показывалось в «Подписание внешней или небезопасной сборки внешним ключом», рис.7.
Перейдем в SQL Server Management Studio и создадим логин, ассоциированный с открытым ключом данной сборки.
use master
if exists (select 1 from sys.server_principals where name = 'SQLCLRLogin') drop login SQLCLRLogin
if exists (select 1 from sys.asymmetric_keys where name = 'SQLCLRKey') drop asymmetric key SQLCLRKey
create asymmetric key SQLCLRKey from executable file = 'C:\Demo\10.MergeFolders\SqlServerProject1\bin\Debug\SqlClassLibrary.dll'
create login SQLCLRLogin from asymmetric key SQLCLRKey
grant external access assembly to SQLCLRLogin
Скрипт 2
Теперь вернемся в VS и продеплоим проект на SQL Server. Либо это можно сделать, оставаясь здесь же, в SSMS:
use tempdb
if exists (select 1 from sys.objects where type_desc = 'CLR_TABLE_VALUED_FUNCTION' and name = 'Dir')
drop function dbo.Dir
if exists (select 1 from sys.procedures where name = 'FileCopy' and type = 'PC') drop proc FileCopy
if exists (select 1 from sys.procedures where name = 'FolderCreate' and type = 'PC') drop proc FolderCreate
if exists (select 1 from sys.assemblies where is_user_defined = 1 and name = 'MyAssembly')
drop assembly MyAssembly
go
create assembly MyAssembly from 'C:\Demo\10.MergeFolders\SqlServerProject1\bin\Debug\SqlClassLibrary.dll' with permission_set = external_access
select * from sys.assembly_files
go
create function dbo.Dir(@folder nvarchar(1000), @shallowTraversal bit) returns table (fullName nvarchar(1000), dateModified datetime2, size bigint, isDir bit, parent nvarchar(1000)) as external name MyAssembly.UserDefinedFunctions.InitMethod
go
-- 1 - только верхняя папка, 0 - рекурсивно вглубь.
select * from dbo.Dir('c:\Temp', 1)
go
create proc FileCopy @sourceFullName nvarchar(1000), @destFullName nvarchar(1000) as external name MyAssembly.StoredProcedures.FileCopy
go
Скрипт 3
Создадим процедуру определения несовпадений.
/* Процедура FindLeftDifferences обнаруживает несовпадения между папками @rootFolderFrom и @rootFolderTo
для синхронизации @rootFolderFrom в @rootFolderTo.
Несовпадения, которые умеет отыскивать процедура:
1 - файла/папки с относительным именем из @rootFolderFrom нет в @rootFolderTo
2 - дата модификации файла в @rootFolderFrom > даты модификации файла с таким же относительным именем в @rootFolderTo
3 - размер файла в @rootFolderFrom не совпадает с размером файла с с таким же относительным именем в @rootFolderTo
Такие объекты считаются кандидатами на копирование из @rootFolderFrom в @rootFolderTo.
В параметре @shallowTraversal задается глубина сканирования.
1 - только непосредственные дети текущего фолдера,
0 - все вложенные подфолдеры до упора.
*/
use tempdb
if exists (select 1 from sys.procedures where name = 'FindLeftDifferences' and schema_id() = schema_id) drop proc FindLeftDifferences
go
create proc FindLeftDifferences @rootFolderFrom nvarchar(1000), @rootFolderTo nvarchar(1000), @shallowTraversal bit as begin
--Полные имена папок источника и назначения должны заканчиваться на \. Если нет, символ добавляется.
set @rootFolderFrom = case when right(@rootFolderFrom, 1) = '\' then @rootFolderFrom else @rootFolderFrom + '\' end
set @rootFolderTo = case when right(@rootFolderTo, 1) = '\' then @rootFolderTo else @rootFolderTo + '\' end
--Создаем таблицу-переменную, куда выполняем CLRную ф-цию Dir (список объектов каталога с необходимыми
--атрибутами). В процессе вставки добавляем относительное имя (fullName минус @rootFolderFrom) и заменяем
--parent тоже на относительный путь родительского каталога.
declare @source table (relativeName nvarchar(1000), fullName nvarchar(1000), dateModified datetime2, size bigint, isDir bit, relParent nvarchar(1000), primary key (isDir, relativeName))
insert @source select substring(fullName, len(@rootFolderFrom) + 1, len(fullName) - len(@rootFolderFrom)),
fullName, dateModified, size, isDir,
substring(parent, len(@rootFolderFrom) + 1, len(fullName) - len(@rootFolderFrom))
from dbo.Dir(@rootFolderFrom, @shallowTraversal)
--То же самое для фолдера назначения, получаем его содержимое.
declare @dest table (relativeName nvarchar(1000), fullName nvarchar(1000), dateModified datetime2, size bigint, isDir bit, relParent nvarchar(1000), primary key (isDir, relativeName))
insert @dest select substring(fullName, len(@rootFolderTo) + 1, len(fullName) - len(@rootFolderTo)),
fullName, dateModified, size, isDir,
substring(parent, len(@rootFolderTo) + 1, len(fullName) - len(@rootFolderTo))
from dbo.Dir(@rootFolderTo, @shallowTraversal)
--Сливаем результаты сравнения содержаний каталогов в таблицу ##merge.
if object_id('tempdb..##merge', 'table') is not null drop table ##merge
--Сопоставление источника и назначения производится по относительному имени, при этом учитывается, файл это или фолдер.
;with
cte as (
--Выявляем файлы и фолдеры источника, которые отсутствуют в назначении. select s.*, cast(1 as tinyint) as reason, cast(1 as tinyint) as CopyStatus from @source s left join @dest d on s.relativeName = d.relativeName and s.isDir = d.isDir where d.fullName is null
union
--Выявляем файлы источника, дата модификации которых позже соответствующего файла в назначении.
select s.*, 2, 1 from @source s inner join @dest d on s.relativeName = d.relativeName and s.isDir = d.isDir and s.isDir = 0 and s.dateModified > d.dateModified
union
--Выявляем файлы источника, размер которых не совпадает с размером соответствующего файла в назначении.
select s.*, 3, 1 from @source s inner join @dest d on s.relativeName = d.relativeName and s.isDir = d.isDir and s.isDir = 0 and s.size <> d.size
)
select row_number() over (order by fullname) as n,
relativeName, convert(char(20), dateModified, 120) as dateModified,
isDir, size, reason, CopyStatus, fullName as source, @rootFolderTo + relativeName as dest
into ##merge from cte
--Таблица ##merge является результатом работы данной процедуры.
--Поле CopyStatus имеет следующие значения:
--0 - файл/фолдер в данной записи не будет копироваться процедурой MergeFolders.
--1 - файл/фолдер в данной записи будет копироваться процедурой MergeFolders.
--2 - файл/фолдер в данной записи скопирован процедурой MergeFolders.
--По умолчанию все выявленные несовпадения, т.е. все записи в таблице ##merge имеют CopyStatus = 1.
--При необходимости его можно откорректировать вручную. CopyStatus = 2 проставляется по мере обработки процедурой MergeFolders.
end
go
Скрипт 4
Создадим процедуру копирования. Это просто. Она пробегается вдоль таблицы ##merge, полученной в результате предыдущей процедуры, и если поле CopyStatus = 1, копирует файл, если это файл, или создает фолдер, если в этой записи лежит фолдер. CopyStatus становится 2 у этой эаписи и у всех с таким же относительным именем. Это нужно, чтобы избежать повторного копирования, т.к., например, источник может быть кандидатом на копирование в случае, если у него позже дата модификации и если его размер разнится с назначением. В этом случае в таблице ##merge будут две записи, соответствующие данному относительному имени.
use tempdb
if exists (select 1 from sys.procedures where name = 'MergeFolders' and schema_id() = schema_id) drop proc MergeFolders
go
create proc MergeFolders as begin
declare @i int = 0, @source nvarchar(1000), @dest nvarchar(1000), @isDir bit
while 1 = 1 begin
select top 1 @i = n, @source = source, @dest = dest, @isDir = isDir from ##merge where CopyStatus = 1 and n > @i
if @@rowcount = 0 break
if @isDir = 1 exec FolderCreate @dest else exec FileCopy @source, @dest
print ''
print cast(@i as varchar(10)) + ') ' + @source + ' -> ' + @dest
update ##merge set CopyStatus = 2 where source = @source
end
end
Скрипт 5
Работа выглядит следующим образом. Выявляются несоответствия между фолдерами:
exec FindLeftDifferences 'C:\Demo\10.MergeFolders', 'C:\Demo\10.MergeFolders - Copy', 0
При желании их можно посмотреть и подправить CopyStatus у тех записей, которые мы в силу каких-то причин не желаем копировать
SELECT relativeName, dateModified, isDir, size, reason, CopyStatus
FROM [##merge]
ORDER BY source
После чего запускаем процедуру слияния
exec MergeFolders
Собственно, все. Буду признателен за выявленные ошибки, поскольку писалось это практически экспромтом вчера перед SQL Server User Group в Самаре. Можете брать, дорабатывать на свой вкус и использовать. Благое дело можно также совершить, прикрутив сюда графический интерфейс на WPF, что придаст примеру товарный вид. Да, чуть не забыл. The last but not the least. Все это действо у нас разворачивалось на SQL Server 2008 Express, который, если кто забыл, между прочим, бесплатный ;)
Comments
Anonymous
May 15, 2009
PingBack from http://microsoft-sharepoint.simplynetdev.com/%d1%81%d0%b8%d0%bd%d1%85%d1%80%d0%be%d0%bd%d0%b8%d0%b7%d0%b0%d1%86%d0%b8%d1%8f-%d1%84%d0%b0%d0%b9%d0%bb%d0%be%d0%b2%d1%8b%d1%85-%d0%ba%d0%b0%d1%82%d0%b0%d0%bb%d0%be%d0%b3%d0%be%d0%b2-%d1%81%d1%80/Anonymous
May 19, 2009
Давайте разберем типовую ошибку, которую совершают слушатели на лабораторках при написании сабж. ЧтобыAnonymous
June 02, 2009
В предыдущей серии картины (http://blogs.msdn.com/alexejs/archive/2009/05/27/hierarchyid-parent-child.aspx)