Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions src/ui/Logic/Plugins/PluginDownloadService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,27 +74,10 @@ await Task.Run(() =>
targetName = SanitizeFolderName(entry.Name);
}

// Remove any previously installed copy of the same plugin.
var existing = _pluginCatalog.GetPlugins()
.FirstOrDefault(p => p.Manifest.Name.Equals(entry.Name, StringComparison.OrdinalIgnoreCase));
if (existing != null && Directory.Exists(existing.FolderPath))
{
Directory.Delete(existing.FolderPath, recursive: true);
}

cancellationToken.ThrowIfCancellationRequested();

var targetPath = Path.Combine(Se.PluginsFolder, targetName);
if (Directory.Exists(targetPath))
{
Directory.Delete(targetPath, recursive: true);
}

// Last chance to abort before the move that publishes the new
// plugin. Cancelling between the deletes above and the move
// would leave the user with no plugin at all.
cancellationToken.ThrowIfCancellationRequested();
Directory.Move(source, targetPath);
PublishPlugin(source, targetPath, existing?.FolderPath, cancellationToken);
}, cancellationToken);
}
finally
Expand All @@ -113,6 +96,30 @@ await Task.Run(() =>
}
}

internal static void PublishPlugin(
string source,
string targetPath,
string? existingPluginPath,
CancellationToken cancellationToken)
{
// This is the commit point for the replacement. Honour cancellation before any
// destructive operation; once an installed copy is removed, finish publishing the
// already-downloaded replacement instead of leaving the user with no plugin at all.
cancellationToken.ThrowIfCancellationRequested();

if (!string.IsNullOrEmpty(existingPluginPath) && Directory.Exists(existingPluginPath))
{
Directory.Delete(existingPluginPath, recursive: true);
}

if (Directory.Exists(targetPath))
{
Directory.Delete(targetPath, recursive: true);
}

Directory.Move(source, targetPath);
}

private static string SanitizeFolderName(string name)
{
var invalid = Path.GetInvalidFileNameChars();
Expand Down
60 changes: 60 additions & 0 deletions tests/UI/Logic/Plugins/PluginDownloadServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Nikse.SubtitleEdit.Logic.Plugins;
using Xunit;

namespace UITests.Logic.Plugins;

public class PluginDownloadServiceTests : IDisposable
{
private readonly string _tempRoot;

public PluginDownloadServiceTests()
{
_tempRoot = Path.Combine(Path.GetTempPath(), "PluginPublish_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempRoot);
}

public void Dispose()
{
if (Directory.Exists(_tempRoot))
{
Directory.Delete(_tempRoot, recursive: true);
}
}

[Fact]
public void PublishPlugin_CancelledBeforeCommitPoint_PreservesInstalledPlugin()
{
var source = Path.Combine(_tempRoot, "new-plugin");
var target = Path.Combine(_tempRoot, "installed-plugin");
Directory.CreateDirectory(source);
Directory.CreateDirectory(target);
File.WriteAllText(Path.Combine(source, "new.txt"), "new");
File.WriteAllText(Path.Combine(target, "old.txt"), "old");

using var cts = new CancellationTokenSource();
cts.Cancel();

Assert.Throws<OperationCanceledException>(() =>
PluginDownloadService.PublishPlugin(source, target, target, cts.Token));

Assert.True(File.Exists(Path.Combine(target, "old.txt")));
Assert.True(File.Exists(Path.Combine(source, "new.txt")));
}

[Fact]
public void PublishPlugin_NotCancelled_ReplacesInstalledPlugin()
{
var source = Path.Combine(_tempRoot, "new-plugin");
var target = Path.Combine(_tempRoot, "installed-plugin");
Directory.CreateDirectory(source);
Directory.CreateDirectory(target);
File.WriteAllText(Path.Combine(source, "new.txt"), "new");
File.WriteAllText(Path.Combine(target, "old.txt"), "old");

PluginDownloadService.PublishPlugin(source, target, target, CancellationToken.None);

Assert.False(Directory.Exists(source));
Assert.True(File.Exists(Path.Combine(target, "new.txt")));
Assert.False(File.Exists(Path.Combine(target, "old.txt")));
}
}