迁移TFS,批量将文档导入SharePoint 2013 文档库
一、需求分析
公司需要将存在于旧系统(TFS)所有的文档迁移至新系统(SharePoint 2013)。现已经将50G以上的文档拷贝到SharePoint 2013 Server上。这些文档是一些不规则的资料,除了常见的Office文件、PDF,还包括图片、RAR等,甚至还包括一些快捷方式(.link)这类的"脏数据"。除此之外,这些存在于TFS中的文档,名称也是"不规则",即包含了SharePoint 2013文档命名不支持的字符如"&", "\"", "?", "<", ">", "#", "{", "}", "%", "~", "/", "\\"。所以,这对导入又增加了复杂度。
了解了文档内容和命名规则后,接下来就是分析怎样导入至SharePoint文档库中:
- 首先,每一个二级文件夹的命名是有规则的,正好是项目编号(Project Number),如GCP-xxxx-xxx-xxx。再根据此编号创建一个子站点。
- 值得一提的是,根据编号创建的子站点并不是随意创建的,而是需要考虑究竟要在哪一个Site Collection下创建子站点,并且还要给予独立权限的分配,即为子站点打断权限继承,为其增加两个组(Owners和Members),并向组里添加对应的人员。
- 对应的创建规则存在于如下List中

其中Project Number即项目编号,与TFS中文件夹的名称一致。Department 即需要将此子站点创建于哪个Site Collection中,包含两个值SMO和CO。PM列是一个Person Or Group类型的字段,需要将此字段的值加入到Owner组,Domain Group列也是一个Person Or Group类型的字段,需要将此字段的值加入到Member组中。
- 接下来,是最重要的一步,找到最佳实践去创建各个Level的文件夹并传入文档。
二、分析和构建导入程序
首先,文件夹的目录结构如下图所示:

文档目录结构图
- 根据上图文档目录结构图,分割字符串(E:\TFS\GCP0401-S\4.Project Management\3 Document Management\TMF),获取文件夹的名称,即Project Number(GCP0401-S)。然后根据此Project Number找到对应的Department、PM、Domain Group,最后创建子站点。逻辑代码如下图所示:
private SPWeb CreateSubSite(string webUrl, string webTitle, string description)
{
var result = (from e in projectInfos where e.ProjectNumber == webUrl && tempArray.Contains(webUrl) select e).FirstOrDefault();
if (result==null)
{
logger.Debug("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&以下Sub Site没有创建********");
logger.Debug("Web Url="+webUrl);
logger.Debug("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&*********");
return null;
}
logger.Debug("开始创建在Site Collection:"+result.Department.ToUpper());
using (SPSite site = result.Department.ToUpper()=="SMO"
? new SPSite("http://sp/sites/smo")
:new SPSite("http://sp/sites/cro"))
{//{*/using(SPSite site=new SPSite("http://reus")){
using (SPWeb web = site.OpenWeb())
{
try
{
SPWeb subWeb = null;
if (site.AllWebs[webUrl].Exists)
{
subWeb = site.AllWebs[webUrl];
}
else
{ logger.Debug("不存在"+webUrl+",则创建新的WebSite");
//不存在则创建新的WebSite
subWeb = site.AllWebs.Add(webUrl, webTitle, description, 1033, "STS#0", true, false); string groupTitleForOwners = subWeb.Title + " Owners";
subWeb.AssociatedOwnerGroup = EnsureGroup(subWeb,groupTitleForOwners , "Full Control"); string groupTitleForMembers = subWeb.Title + " Members";
subWeb.AssociatedMemberGroup = EnsureGroup(subWeb,groupTitleForMembers , "Edit"); subWeb.Groups[groupTitleForOwners].AddUser(subWeb.Site.Owner); if (result.PM!=null)
{
subWeb.Groups[groupTitleForOwners].AddUser(result.PM);
}
if (result.DomainGroup!=null)
{
subWeb.Groups[groupTitleForMembers].AddUser(result.DomainGroup);
} subWeb.Update();
logger.Debug(webUrl+"创建成功");
richTextBox2.Text += webUrl + "创建OK" + "\r\n";
} return subWeb;
}
catch (Exception es)
{
logger.Debug(es.ToString());
return null;
} }
} }
- 从上文档目录结构图可以分析出,二级目录是项目编号,即对应要创建的子站点。在此目录下有"无限级"的子文件夹。那应该怎样在子站点的文档库中创建如此多的文件夹呢,这需要好好考虑一下。对,用递归,得到每一个分支最底层的文件夹路径即可。具体实现如下所示:
private void GetDeepestFoleder(string sDir)
{
string dir = string.Empty;
try
{
foreach (string path in Directory.GetDirectories(sDir))
{
dir = path; if (!arrayList.Contains(dir))
{
arrayList.Add(dir);
} arrayList.Remove(dir.Substring(, dir.LastIndexOf("\\"))); GetDeepestFoleder(dir);
}
}
catch (Exception excpt)
{
logger.Debug(excpt.ToString());
}
}
- 得到所有最内层文件夹的URL之后,接着就是在SharePoint 文档库中创建一级一级的文件夹了。
private void CreateForderForDocumentLibrary(string folderUrl, SPWeb currentWeb)
{
try
{
SPFolder newFolder = null;
string spFolderUrl = currentWeb.ServerRelativeUrl + "/Shared Documents";
logger.Debug("SPFolder Url="+spFolderUrl);
//分割字符串,得到父子Folder的Url,在文档库中创建文件夹
foreach (string strUrl in folderUrl.Split('\\'))
{ //todo:有空格会报错吗?
string tempStrUrl = strUrl.Trim();
//SharePoint 文档库中文件名有严格的格式要求
var r = new[] { "&", "\"", "?", "<", ">", "#", "{", "}", "%", "~", "/", "\\"}.Any(tempStrUrl.Contains);
if (r)
{
tempStrUrl = tempStrUrl.Clean();
} if (tempStrUrl.StartsWith(".") || tempStrUrl.EndsWith("."))
{
tempStrUrl = tempStrUrl.Replace(".", "-");
} spFolderUrl += "/" + tempStrUrl;
logger.Debug("SPFolder Url="+spFolderUrl);
SPList list = currentWeb.Lists.TryGetList("Documents");
SPFolderCollection folderCollection = list.RootFolder.SubFolders;
newFolder = folderCollection.Add(spFolderUrl);
logger.Debug(spFolderUrl + "创建成功");
}
}
catch (Exception ex)
{
logger.Debug(ex.ToString());
throw;
}
}
- 以上代码逻辑中包含了字符串的处理,因为SharePoint 2013的文档、文件夹命名有严格的要求,不能包含非法字符。并且也不能以字符 "."开始或者结束。所以添加了字符串处理操作功能。
public static class MyStringExtention
{
public static string Clean(this string s)
{
StringBuilder sb = new StringBuilder(s);
//"&", "\"", "?", "<", ">", "#", "{", "}", "%", "~", "/", "\\" uu.uu可以,但uu.就不行
sb.Replace("&", "-");
sb.Replace("\"", "-");
sb.Replace("?", "-");
sb.Replace("<", "-");
sb.Replace(">", "-");
sb.Replace("#", "-");
sb.Replace("{", "-");
sb.Replace("}", "-");
sb.Replace("%", "-");
sb.Replace("~", "-");
sb.Replace("/", "-");
sb.Replace("\\", "-");
// sb.Replace(".", "-"); return sb.ToString();
}
}
- 在成功创建了子站点并在文档库中创建了所有文件夹后,接下来就是将文档上传至指定的文件夹中了。所以接下来,需要获取指定目录下所有的文件,我使用了一个队列来保存文件路径,而不是使用递归或者使用.NET 4.0提供的基于文件迭代的功能(Directory.EnumerateFiles)来获取所有文件,原因有2点:
- Directory.EnumerateFiles内置的递归方法容易抛出异常,比如没有权限访问等。
- Queue<String> 避免了多次递归时调用堆栈从而会创建大数组。
private IEnumerable<string> GetFiles(string path)
{
Queue<string> queue = new Queue<string>();
queue.Enqueue(path);
while (queue.Count > )
{
path = queue.Dequeue();
try
{
foreach (string subDir in Directory.GetDirectories(path))
{
queue.Enqueue(subDir);
}
}
catch (Exception ex)
{
logger.Debug("===========发生错误啦*========================");
logger.Debug(ex.ToString());
logger.Debug("===========发生错误了========================");
}
string[] files = null;
try
{
files = Directory.GetFiles(path);
}
catch (Exception ex)
{
logger.Debug("===========发生错误啦&========================");
logger.Debug(ex.ToString());
logger.Debug("===========发生错误了========================");
}
if (files != null)
{
for (int i = ; i < files.Length; i++)
{
yield return files[i];
}
}
}
}
- 在获取了所有文件之后,上传至指定文档库即可,别忘记处理非法字符。
private void UploadFileToDocumentLibrary(string folderUrl, string filePath, SPWeb currentWeb)
{ try
{
//todo:不同文件名字相同会覆盖吗todo
SPFolder newFolder = null;
string spFolderUrl = currentWeb.ServerRelativeUrl+ "/Shared Documents";
//极端情况 GCP0117 TFS List.xls
//分割字符串,得到父子Folder的Url,在文档库中创建文件夹
foreach (string strUrl in folderUrl.Split('\\'))
{ //todo:有空格会报错吗?
string tempStrUrl = strUrl.Trim();
//SharePoint 文档库中文件名有严格的格式要求
var result = new[] { "&", "\"", "?", "<", ">", "#", "{", "}", "%", "~", "/", "\\" }.Any(tempStrUrl.Contains);
if (result)
{
tempStrUrl = tempStrUrl.Clean();
} if (tempStrUrl.StartsWith(".") || tempStrUrl.EndsWith("."))
{
tempStrUrl = tempStrUrl.Replace(".", "-");
} spFolderUrl += "/" + tempStrUrl;
}
newFolder = currentWeb.GetFolder(spFolderUrl); string fileName = System.IO.Path.GetFileName(filePath);
//SharePoint 文档库中文件名有严格的格式要求
var r=new[] { "&", "\"", "?", "<", ">", "#", "{", "}", "%", "~", "/", "\\"}.Any(fileName.Contains);
if (r)
{
logger.Debug("***********File Name包含了Invalid Value***********************");
logger.Debug("***********File Name="+fileName);
logger.Debug("***********File Path="+filePath); fileName = fileName.Clean();
logger.Debug("*********替换过后的File Name************************************");
logger.Debug(fileName);
} if (fileName.StartsWith(".") || fileName.EndsWith("."))
{
fileName=fileName.Replace(".", "-");
} //判断文件是否已经存在,若存在,则不再重复上传 string spFileUrl = spFolderUrl + "/" + fileName;
SPFile newSpFile = currentWeb.GetFile(spFileUrl);
if (!newSpFile.Exists)
{
using (FileStream fs = File.OpenRead(filePath))
{
byte[] contentBytes = new byte[fs.Length];
fs.Read(contentBytes, 0, (int)fs.Length);
if (newFolder != null)
{
SPFile spFile = newFolder.Files.Add(fileName, contentBytes, true);
newFolder.Update();
} }
logger.Debug(fileName + "上传成功 and FilePath=" + filePath);
}
else
{
logger.Debug(spFileUrl+"已存在");
} }
catch (Exception ex)
{
logger.Debug(ex.ToString());
throw;
}
}
三、异常处理
主要发生的异常是文件名包含Invalid字符,对SharePoint而言,文档库Folder和File的名字都有严格的限制,不能包含#、%等,现在处理异常是记录到日志然后手动去修改名称。
- 报错的异常


- 将异常记录至日志里,方便修改。

四、检查是否导入成功
- 导入成功界面

- 检查日志

- 登陆系统,检查是否全部导入,并且检查权限设置是否正确。

- 查看文件夹和文档是否成功创建和上传

迁移TFS,批量将文档导入SharePoint 2013 文档库的更多相关文章
- sharepoint 2013 列表和库标签 元数据导航配置(2)
接前面提到的,如何创建一个术语库.sharepoint 2013 列表和库标签 元数据导航配置(1), 现在要做的,就是如何在自定义或者文档库中使用这个术语库,实现标签功能,通过这些标签,找到对应的文 ...
- SharePoint 2013 文档库中PPT转换PDF
通过使用 PowerPoint Automation Services,可以从 PowerPoint 二进制文件格式 (.ppt) 和 PowerPoint Open XML 文件格式 (.pptx) ...
- SharePoint 2013 文档上传的多种形式
SharePoint 2013 中的某些功能需要使用 ActiveX 控件.这会在不支持 ActiveX 的浏览器上产生限制.目前只有 32 位版本的 Internet Explorer 支持此功能. ...
- sharepoint 2013 文档库 资源管理器打开报错 在文件资源管理器中打开此位置时遇到问题,将此网站添加到受信任站点列表,然后重试。
我们在使用sharepoint 2013的文档库或者资源库的时候,经常会需要用到使用“资源管理器”来管理文档,但是有时候,点击“使用资源管理器打开”,会提示如下错误: 在文件资源管理器中打开此位置时遇 ...
- sharepoint 2013 文档库eventhandle权限控制
记录一下如何在sharepoint server 2013文档库中,使用eventhandle控制文档库document library的条目item权限. ///<summary> // ...
- SharePoint 2013 文档库“样式”变了
有朋友反馈说文档库的样式变了. 经查证,原来有人修改了视图的"样式":库设置—视图—样式,改为默认即可. 另外,如果编辑页面,编辑web部件的属性,在"杂项"勾 ...
- 启用SharePoint 2013文档版本控制
cls $PSSnapin = Add-PsSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue | Out-Nul ...
- 跟我学SharePoint 2013视频培训课程——签出、签入文档(9)
课程简介 第9天,怎样在SharePoint 2013中签出.签入文档 视频 SharePoint 2013 交流群 41032413
- SharePoint 2013 入门教程
以下文章是自己在学习SharePoint的过程中,不断积累和总结的博文,现在总结一个目录,分享给大家.这个博客也是自己从SharePoint入门,到一个SharePoint开发的成长记录,里面记录的都 ...
随机推荐
- ActiveMQ面试专题
https://blog.csdn.net/belvine/article/details/79399798
- 1073: 动物简介(animal)
#include<iostream> #include<string> #include<stdio.h> #include<algorithm> #i ...
- 把文件(项目)上传到Git@OSC
说明: 登录与配置git的操作此处忽略. 假设你已配置好git,并且已绑定oschina. 下面是把一个本地文件夹上传到git的操作: 在git@osc上新建项目.记得选择添加对应的 .gitigno ...
- 7、java5线程池之单一线程池newSingleThreadExecutor
JDK文档说明: 创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程.(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执 ...
- Quartz.NET开源作业调度框架系列(四):Plugin Job-转
如果在Quartz.NET作业运行时我们想动态修改Job和Trigger的绑定关系,同时修改一些参数那么该怎么办呢?Quartz.NET提供了插件技术,可以通过在XML文件中对Job和Trigger的 ...
- Quartz.NET开源作业调度框架系列(一):快速入门step by step-转
Quartz.NET是一个被广泛使用的开源作业调度框架 , 由于是用C#语言创建,可方便的用于winform和asp.net应用程序中.Quartz.NET提供了巨大的灵活性但又兼具简单性.开发人员可 ...
- 转:OGRE场景管理器介绍
一个场景代表在虚拟世界中显示的物品.场景可以包括静态几何体(比如地形或者室内),模型(比如树.椅子等),光和摄像机.场景有下面种类.室内场景:可能由走廊.有家具的屋子和挂着装饰品的墙组成.室外场景:可 ...
- 【onethink1.0】HTML模板获取前台和后台当前登录用户名
1.版本:onethink1.0:位置:HTML模板 前台:{:get_username()} 后台:{:session('user_auth.username')}
- mysql配置和管理(转载)
MySQL的配置与管理 (2012-08-09 13:06:59) 转载▼ 标签: mysql 配置 管理 分类: MySQL 安装 yum -y install mysql-server 启动服 ...
- Android轻量级ORM框架ActiveAndroid入门教程(转)
注:没有找到出处,如有侵犯,请告知 开始ActiveAndroid神奇之旅: 在AndroidManifest.xml中我们需要添加这两个 AA_DB_NAME (数据库名称,这个name不能改,但是 ...