迁移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,最后创建子站点。逻辑代码如下图所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | 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(0, dir.LastIndexOf("\\"))); GetDeepestFoleder(dir); } } catch (Exception excpt) { logger.Debug(excpt.ToString()); } }
- 得到所有最内层文件夹的URL之后,接着就是在SharePoint 文档库中创建一级一级的文件夹了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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 > 0) { 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 = 0; i < files.Length; i++) { yield return files[i]; } } } }
- 在获取了所有文件之后,上传至指定文档库即可,别忘记处理非法字符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | 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的名字都有严格的限制,不能包含#、%等,现在处理异常是记录到日志然后手动去修改名称。
- 报错的异常
- 将异常记录至日志里,方便修改。
四、检查是否导入成功
- 导入成功界面
- 检查日志
- 登陆系统,检查是否全部导入,并且检查权限设置是否正确。
- 查看文件夹和文档是否成功创建和上传
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
2013-03-23 ASP.NET那点不为人知的事(三)