强制 SharePoint 文档解锁/签入
强制解锁已签出/锁定的文档,即使是短期锁定。
引言
当您签出或打开文档库中的文件时,SharePoint 会对其进行锁定。有时用户 PC 崩溃,或者他们失去互联网连接,或者太阳耀斑击中服务器机房,管理员无论如何尝试都无法释放锁定。SharePoint 将此称为 CheckOutStatus
,在本文中,我将称之为锁定,因为这正是该状态所描述的内容。
一些人已经在一个控制台应用程序中编写了代码来强制签入文档。但这仅适用于长期锁定(签出)。当文档未签出但客户端应用程序正在锁定它时,会放置一个短期锁定。如果由于“背景”部分中解释的原因,短期锁定未被释放,您要么必须重新启动服务器,要么等到锁定过期。
客户端应用程序可能会更新签出的过期时间;例如,Office 程序每 10 分钟更新一次锁定。如果客户端程序不更新锁定,SharePoint 应该会释放它。但是,如果客户端应用程序崩溃或出现其他几种情况,人们最终会等待数小时甚至数天才能解锁文档。据我所知,**无法**通过 API 解锁短期锁定(可能是故意的)。
使用此应用程序,您可以快速轻松地解锁长期和短期锁定。
背景
您可能想阅读这篇博文,其中介绍了 SharePoint 锁定类型。它很好地描述了签出状态以及何时应用它们。
关于短期锁定的说明
有时在尝试签出文档时,您会收到一条错误消息,说“文件 [文件名] 已被 [用户名] 签出或锁定进行编辑”;如果您的身份验证 cookie 已过期,有时 [用户名] 就是您自己!微软在此处很好地记录了此错误,但唯一的解决方法是坐等 SharePoint 解锁它,这应该需要 10 分钟,但有时需要数小时或数天!
如 KB 文章中所述,当您尝试在客户端程序中编辑文档然后程序崩溃时,就会发生这种情况。此外,我发现使用 WebDAV(资源管理器)视图打开文档而不签出时会发生这种情况。如果不先签出文档再进行编辑,也可以通过网站发生这种情况。
通常,您可以关闭客户端应用程序,短期锁定就会被释放,但有时,特别是在身份验证 cookie 过期或应用程序崩溃时,会出现问题,锁定不会被释放。或者也许那个不乖的用户在签出文件之前就打开了文件然后去吃午饭了,那么如果您无法进入他们的计算机,就无法强制释放它!即使关闭计算机有时也无效。
我见过这导致了一些奇怪的事情。当您尝试签出文档时,您可能会收到一条错误消息,说“文件 [文件名] 未被签出。”!!当然,这正是我要签出它的原因……我对此不太理解,但我认为 SharePoint 正在尝试签出文档,意识到它具有 ShortTerm
的 CheckOutStatus
,并尝试将其签入(猜测)。
Using the Code
您必须从 SharePoint 服务器运行此代码,因为它使用 SharePoint 对象模型而不是 Web 服务。当您打开程序时,只需输入站点 URL(包括网站集)、文档库的名称(如 URL 中所示,而不是显示名称)、文档的完整文件名,然后点击“查找”即可找到文档并获取其签出状态信息。
这是代码
private void FindButton_Click(object sender, EventArgs e)
{
this.StatusField.Text = "Searching..." + Environment.NewLine;
this.StatusField.Refresh();
this.LockStatusField.Text = string.Empty;
try
{
Uri siteUri = new Uri(this.SiteUrlField.Text.Trim());
using (SPWeb web = new SPSite(siteUri.ToString()).OpenWeb())
{
bool libraryFound = false;
foreach (SPList list in web.Lists)
{
if (list.BaseType == SPBaseType.DocumentLibrary && string.Equals(
list.RootFolder.Url, this.libraryField.Text.Trim(),
StringComparison.OrdinalIgnoreCase))
{
libraryFound = true;
this.statusField.Text += "Found library. Searching for document..." +
Environment.NewLine;
this.statusField.Refresh();
SPQuery NameQuery = new SPQuery();
NameQuery.Query =
"<Where><Eq><FieldRef Name='FileLeafRef' /><Value Type='File'>" +
this.filenameField.Text.Trim() + "</Value></Eq></Where>";
NameQuery.RowLimit = 1;
if (recursiveField.Checked)
NameQuery.ViewAttributes = @"Scope=""Recursive""";
if (recursiveAllField.Checked)
NameQuery.ViewAttributes = @"Scope=""RecursiveAll""";
if (!string.IsNullOrEmpty(folderNameField.Text.Trim()))
{
try
{
SPFolder parentFolder = list.ParentWeb.GetFolder(
siteUri.ToString() + "/" + list.RootFolder.Url + "/" +
folderNameField.Text.Trim());
this.statusField.Text += "Found folder." + Environment.NewLine;
NameQuery.Folder = parentFolder;
}
catch (Exception)
{
this.statusField.Text += "Could not find folder: '" +
siteUri.ToString() + "/" + list.RootFolder.Url + "/" +
folderNameField.Text.Trim();
}
}
SPListItemCollection documents = list.GetItems(NameQuery);
if (documents.Count > 0 && documents[0].Name.ToLower() ==
filenameField.Text.Trim().ToLower())
{
SPListItem item = documents[0];
this.statusField.Text += "Found document at: '" + item.Url + "'" +
Environment.NewLine;
ReportItemCheckoutStatus(item);
if (item.File.CheckOutStatus != SPFile.SPCheckOutStatus.None)
this.unlockButton.Enabled = true;
else
this.unlockButton.Enabled = false;
itemToUnlock = item;
}
else
{
statusField.Text += "Could not find document: '" +
this.filenameField.Text.Trim() + "'.";
}
break;
}
}
if (libraryFound == false)
StatusField.Text += "Could not find library: '" +
this.LibraryField.Text.Trim() + "'.";
}
}
catch (Exception ex)
{
StatusField.Text += Environment.NewLine + "Error occurred. Details: " +
Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace;
}
}
它从 URL 中查找 SPWeb
,遍历 Web 中的所有列表(我发现遍历比使用 GetList()
更快),构建 CAML 查询来查找文档,如果找到文档,则报告其签出状态信息。
“解锁”按钮根据是长期锁定还是短期锁定执行不同的操作。如果它是长期锁定,它只会强制将文档签回。如果它是短期锁定,它必须访问内容数据库。
免责声明:Microsoft 不推荐直接修改 SharePoint 内容数据库,也不支持这样做(但在此情况下效果很好)。
如果文件有短期锁定,我能找到的唯一解决方案是通过直接修改内容数据库来释放它。它对 SharePoint 文档的 AllDocs 表中的 CheckoutExpires 列进行简单的更新。
private void UnlockButton_Click(object sender, EventArgs e)
{
if (itemToUnlock == null)
{
this.StatusField.Text += "No file to unlock!" + Environment.NewLine;
return;
}
this.StatusField.Text = "Unlocking..." + Environment.NewLine;
this.StatusField.Refresh();
if (itemToUnlock.File.CheckOutStatus == SPFile.SPCheckOutStatus.ShortTerm)
{
if (string.IsNullOrEmpty(itemToUnlock.UniqueId.ToString()))
{
StatusField.Text += "File has a short-term lock and " +
"UniqueId cannot be determined for database update.";
return;
}
StatusField.Text += "Unsupported and not recommended WARNING." + Environment.NewLine;
string message = "WARNING!! The selected file has a short-term " +
"lock on it. It should be released in 10 minutes. " +
"You can force the item to be unlocked by setting " +
"the checkout to expire immediately in the content database. " +
"Modifying the database is not supported by Microsoft " +
"and is not recommended. Do you wish to cancel?";
if (MessageBox.Show(message, "WARNING",
MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button1) == DialogResult.No)
{
try
{
UpdateItemCheckoutExpiration(itemToUnlock);
}
catch (Exception ex)
{
StatusField.Text += Environment.NewLine +
"Error occurred. Details: " +
Environment.NewLine + ex.Message +
Environment.NewLine + ex.StackTrace;
return;
}
StatusField.Text += "Database updated. File should now be unlocked.";
// wait 2 seconds for the file to get unlocked
System.Threading.Thread.Sleep(2000);
ReportItemCheckoutStatus(itemToUnlock);
}
}
else if (itemToUnlock.File.CheckOutStatus != SPFile.SPCheckOutStatus.None)
{ // long-term lock, check it in manually
try
{
itemToUnlock.File.CheckIn("Forced Checkin");
this.StatusField.Text += "File checked in." + Environment.NewLine;
ReportItemCheckoutStatus(itemToUnlock);
}
catch (Exception ex)
{
StatusField.Text += Environment.NewLine + "Error occurred. Details: " +
Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace;
return;
}
}
else
{
this.StatusField.Text += "File doesn't have a lock." + Environment.NewLine;
}
if (itemToUnlock.File.CheckOutStatus == SPFile.SPCheckOutStatus.None)
this.UnlockButton.Enabled = false;
}
这是更新内容数据库的代码
private void UpdateItemCheckoutExpiration(SPListItem item)
{
SqlConnection contentDatabaseConnection = null;
try
{
contentDatabaseConnection = new SqlConnection(
item.Web.Site.ContentDatabase.DatabaseConnectionString);
contentDatabaseConnection.Open();
string UpdateCommandText = string.Format("UPDATE dbo.AllDocs SET " +
"CheckoutExpires = '{0:yyyy-MM-dd HH:mm:ss:fff}' WHERE Id = '{1}'",
DateTime.Now.ToUniversalTime(), item.UniqueId.ToString());
SqlCommand UpdateCommand = new SqlCommand(UpdateCommandText,
contentDatabaseConnection);
SqlDataAdapter contentDataAdapter = new SqlDataAdapter();
contentDataAdapter.UpdateCommand = UpdateCommand;
contentDataAdapter.UpdateCommand.ExecuteNonQuery();
contentDatabaseConnection.Close();
}
catch (Exception)
{
throw;
}
finally
{
if (contentDatabaseConnection != null &&
contentDatabaseConnection.State != ConnectionState.Closed)
contentDatabaseConnection.Close();
}
}
关注点
微软建议简单地“等待 10 分钟”来修复此错误是荒谬的,而且很多时候根本不起作用。我花了将近一天的时间来寻找解决这个问题的方法,我希望我能为其他人节省在这方面的挫败感和时间。
历史
- 首次发布。
- 更新于 2010 年 10 月 28 日:添加了递归控件,并调整了代码以递归搜索文档库中的文件夹。添加了搜索特定文件夹以查找文件的功能。
- 更新于 2012 年 6 月 14 日:在 ReportItemCheckoutStatus 中添加了异常处理,以处理在尝试调用 CheckedOutDate 时 SharePoint 出现的愚蠢异常。
- 更新于 2013 年 3 月 8 日:修复了尝试搜索 URL 中带有空格的站点时出现的错误。添加了一个清单,要求在启用 UAC 时进行 UAC 提升(在未以管理员权限运行时修复安全错误)。