diff --git a/src/Orchard.Tests/FileSystems/LockFile/LockFileManagerTests.cs b/src/Orchard.Tests/FileSystems/LockFile/LockFileManagerTests.cs
new file mode 100644
index 000000000..e1493bdd4
--- /dev/null
+++ b/src/Orchard.Tests/FileSystems/LockFile/LockFileManagerTests.cs
@@ -0,0 +1,116 @@
+using System.IO;
+using System.Linq;
+using NUnit.Framework;
+using Orchard.FileSystems.AppData;
+using Orchard.FileSystems.LockFile;
+using Orchard.Tests.Stubs;
+
+namespace Orchard.Tests.FileSystems.LockFile {
+ [TestFixture]
+ public class LockFileManagerTests {
+ private string _tempFolder;
+ private IAppDataFolder _appDataFolder;
+ private ILockFileManager _lockFileManager;
+ private StubClock _clock;
+
+ public class StubAppDataFolderRoot : IAppDataFolderRoot {
+ public string RootPath { get; set; }
+ public string RootFolder { get; set; }
+ }
+
+ public static IAppDataFolder CreateAppDataFolder(string tempFolder) {
+ var folderRoot = new StubAppDataFolderRoot {RootPath = "~/App_Data", RootFolder = tempFolder};
+ var monitor = new StubVirtualPathMonitor();
+ return new AppDataFolder(folderRoot, monitor);
+ }
+
+ [SetUp]
+ public void Init() {
+ _tempFolder = Path.GetTempFileName();
+ File.Delete(_tempFolder);
+ _appDataFolder = CreateAppDataFolder(_tempFolder);
+
+ _clock = new StubClock();
+ _lockFileManager = new DefaultLockFileManager(_appDataFolder, _clock);
+ }
+
+ [TearDown]
+ public void Term() {
+ Directory.Delete(_tempFolder, true);
+ }
+
+ [Test]
+ public void LockShouldBeGrantedWhenDoesNotExist() {
+ ILockFile lockFile = null;
+ var granted = _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile);
+
+ Assert.That(granted, Is.True);
+ Assert.That(lockFile, Is.Not.Null);
+ Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.True);
+ Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(1));
+ }
+
+ [Test]
+ public void ExistingLockFileShouldPreventGrants() {
+ ILockFile lockFile = null;
+ _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile);
+
+ Assert.That(_lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile), Is.False);
+ Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.True);
+ Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(1));
+ }
+
+ [Test]
+ public void ReleasingALockShouldAllowGranting() {
+ ILockFile lockFile = null;
+ _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile);
+
+ using (lockFile) {
+ Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.True);
+ Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(1));
+ }
+
+ Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.False);
+ Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(0));
+ }
+
+ [Test]
+ public void ReleasingAReleasedLockShouldWork() {
+ ILockFile lockFile = null;
+ _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile);
+
+ Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.True);
+ Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(1));
+
+ lockFile.Release();
+ Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.False);
+ Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(0));
+
+ lockFile.Release();
+ Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.False);
+ Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(0));
+ }
+
+ [Test]
+ public void ExpiredLockShouldBeAvailable() {
+ ILockFile lockFile = null;
+ _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile);
+
+ _clock.Advance(DefaultLockFileManager.Expiration);
+ Assert.That(_lockFileManager.IsLocked("foo.txt.lock"), Is.False);
+ Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(1));
+ }
+
+ [Test]
+ public void ShouldGrantExpiredLock() {
+ ILockFile lockFile = null;
+ _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile);
+
+ _clock.Advance(DefaultLockFileManager.Expiration);
+ var granted = _lockFileManager.TryAcquireLock("foo.txt.lock", ref lockFile);
+
+ Assert.That(granted, Is.True);
+ Assert.That(_appDataFolder.ListFiles("").Count(), Is.EqualTo(1));
+ }
+ }
+}
diff --git a/src/Orchard.Tests/Orchard.Framework.Tests.csproj b/src/Orchard.Tests/Orchard.Framework.Tests.csproj
index 7c7b052db..4064da875 100644
--- a/src/Orchard.Tests/Orchard.Framework.Tests.csproj
+++ b/src/Orchard.Tests/Orchard.Framework.Tests.csproj
@@ -245,6 +245,7 @@
+
diff --git a/src/Orchard/Environment/OrchardStarter.cs b/src/Orchard/Environment/OrchardStarter.cs
index 30988def7..5f4b1e1c9 100644
--- a/src/Orchard/Environment/OrchardStarter.cs
+++ b/src/Orchard/Environment/OrchardStarter.cs
@@ -18,6 +18,7 @@ using Orchard.Environment.Descriptor;
using Orchard.Events;
using Orchard.FileSystems.AppData;
using Orchard.FileSystems.Dependencies;
+using Orchard.FileSystems.LockFile;
using Orchard.FileSystems.VirtualPath;
using Orchard.FileSystems.WebSite;
using Orchard.Logging;
@@ -55,6 +56,7 @@ namespace Orchard.Environment {
RegisterVolatileProvider(builder);
RegisterVolatileProvider(builder);
+ RegisterVolatileProvider(builder);
RegisterVolatileProvider(builder);
RegisterVolatileProvider(builder);
RegisterVolatileProvider(builder);
diff --git a/src/Orchard/FileSystems/LockFile/DefaultLockFileManager.cs b/src/Orchard/FileSystems/LockFile/DefaultLockFileManager.cs
new file mode 100644
index 000000000..28ab27b71
--- /dev/null
+++ b/src/Orchard/FileSystems/LockFile/DefaultLockFileManager.cs
@@ -0,0 +1,55 @@
+using System;
+using Orchard.FileSystems.AppData;
+using Orchard.Services;
+
+namespace Orchard.FileSystems.LockFile {
+ public class DefaultLockFileManager : ILockFileManager {
+ private readonly IAppDataFolder _appDataFolder;
+ private readonly IClock _clock;
+
+ public static TimeSpan Expiration { get; private set; }
+
+ public DefaultLockFileManager(IAppDataFolder appDataFolder, IClock clock) {
+ _appDataFolder = appDataFolder;
+ _clock = clock;
+ Expiration = TimeSpan.FromMinutes(10);
+ }
+
+ public bool TryAcquireLock(string path, ref ILockFile lockFile) {
+ try {
+ if(IsLocked(path)) {
+ return false;
+ }
+
+ lockFile = new LockFile(_appDataFolder, path, _clock.UtcNow.ToString());
+ return true;
+ }
+ catch {
+ // an error occured while reading/creating the lock file
+ return false;
+ }
+ }
+
+ public bool IsLocked(string path) {
+ try {
+ if (_appDataFolder.FileExists(path)) {
+ var content = _appDataFolder.ReadFile(path);
+
+ DateTime creationUtc;
+ if (DateTime.TryParse(content, out creationUtc)) {
+ // if expired the file is not removed
+ // it should be automatically as there is a finalizer in LockFile
+ // or the next taker can do it, unless it also fails, again
+ return creationUtc.Add(Expiration) > _clock.UtcNow;
+ }
+ }
+ }
+ catch {
+ // an error occured while reading the file
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/Orchard/FileSystems/LockFile/ILockFile.cs b/src/Orchard/FileSystems/LockFile/ILockFile.cs
new file mode 100644
index 000000000..69e5ee5cc
--- /dev/null
+++ b/src/Orchard/FileSystems/LockFile/ILockFile.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace Orchard.FileSystems.LockFile
+{
+ public interface ILockFile : IDisposable {
+ void Release();
+ }
+}
diff --git a/src/Orchard/FileSystems/LockFile/ILockFileManager.cs b/src/Orchard/FileSystems/LockFile/ILockFileManager.cs
new file mode 100644
index 000000000..b1f242861
--- /dev/null
+++ b/src/Orchard/FileSystems/LockFile/ILockFileManager.cs
@@ -0,0 +1,26 @@
+using Orchard.Caching;
+
+namespace Orchard.FileSystems.LockFile {
+ ///
+ /// Abstraction for lock files creation.
+ ///
+ ///
+ /// All virtual paths passed in or returned are relative to "~/App_Data".
+ ///
+ public interface ILockFileManager : IVolatileProvider {
+ ///
+ /// Attempts to acquire an exclusive lock file.
+ ///
+ /// The filename of the lock file to create.
+ /// A reference to the lock file object if the lock is granted.
+ /// true if the lock is granted; otherwise, false.
+ bool TryAcquireLock(string path, ref ILockFile lockFile);
+
+ ///
+ /// Wether a lock file is already existing.
+ ///
+ /// The filename of the lock file to test.
+ /// true if the lock file exists; otherwise, false.
+ bool IsLocked(string path);
+ }
+}
diff --git a/src/Orchard/FileSystems/LockFile/LockFile.cs b/src/Orchard/FileSystems/LockFile/LockFile.cs
new file mode 100644
index 000000000..1ecb594e3
--- /dev/null
+++ b/src/Orchard/FileSystems/LockFile/LockFile.cs
@@ -0,0 +1,60 @@
+using System;
+using Orchard.FileSystems.AppData;
+
+namespace Orchard.FileSystems.LockFile {
+ ///
+ /// Represents a Lock File acquire on the file system
+ ///
+ public class LockFile : ILockFile {
+ private readonly IAppDataFolder _appDataFolder;
+ private readonly string _path;
+ private readonly string _content;
+ private bool _released;
+
+ public LockFile(IAppDataFolder appDataFolder, string path, string content) {
+ _appDataFolder = appDataFolder;
+ _path = path;
+ _content = content;
+
+ // create the physical lock file
+ _appDataFolder.CreateFile(path, content);
+ }
+
+ public void Dispose() {
+ // dispose both managed and unmanaged resources
+ Dispose(true);
+
+ // don't call the finalizer if dispose is called
+ GC.SuppressFinalize(this);
+ }
+
+ public void Release() {
+ Dispose(true);
+ }
+
+ protected virtual void Dispose(bool disposing) {
+ if(disposing) {
+ // release managed code here
+ // nothing right now, just a placeholder to preserve the pattern
+ }
+
+ if (_released || !_appDataFolder.FileExists(_path)) {
+ // nothing to do, night happen if re-granted, and already released
+ return;
+ }
+
+ _released = true;
+
+ // check it has not been granted in the meantime
+ var current = _appDataFolder.ReadFile(_path);
+ if (current == _content) {
+ _appDataFolder.DeleteFile(_path);
+ }
+ }
+
+ ~LockFile() {
+ // dispose unmanaged resources (file)
+ Dispose(false);
+ }
+ }
+}
diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj
index 32067969b..92bb5bf98 100644
--- a/src/Orchard/Orchard.Framework.csproj
+++ b/src/Orchard/Orchard.Framework.csproj
@@ -183,6 +183,10 @@
+
+
+
+