mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-06 16:04:38 +01:00
File Watcher: Prevent export loopback
This commit is contained in:
parent
d3cc5b0b58
commit
c503aa2a14
6 changed files with 70 additions and 16 deletions
|
|
@ -71,6 +71,7 @@ public partial class Configuration : IPluginConfiguration, ISavable, IService
|
|||
public bool DefaultTemporaryMode { get; set; } = false;
|
||||
public bool EnableDirectoryWatch { get; set; } = false;
|
||||
public bool EnableAutomaticModImport { get; set; } = false;
|
||||
public bool PreventExportLoopback { get; set; } = true;
|
||||
public bool EnableCustomShapes { get; set; } = true;
|
||||
public PcpSettings PcpSettings = new();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods.Editor;
|
||||
|
|
@ -11,15 +12,17 @@ public class ModBackup
|
|||
|
||||
public static bool CreatingBackup { get; private set; }
|
||||
|
||||
private readonly Mod _mod;
|
||||
public readonly string Name;
|
||||
public readonly bool Exists;
|
||||
private readonly ModExportManager _modExport;
|
||||
private readonly Mod _mod;
|
||||
public readonly string Name;
|
||||
public readonly bool Exists;
|
||||
|
||||
public ModBackup(ModExportManager modExportManager, Mod mod)
|
||||
{
|
||||
_mod = mod;
|
||||
Name = Path.Combine(modExportManager.ExportDirectory.FullName, _mod.ModPath.Name) + ".pmp";
|
||||
Exists = File.Exists(Name);
|
||||
_modExport = modExportManager;
|
||||
_mod = mod;
|
||||
Name = Path.Combine(modExportManager.ExportDirectory.FullName, _mod.ModPath.Name) + ".pmp";
|
||||
Exists = File.Exists(Name);
|
||||
}
|
||||
|
||||
/// <summary> Migrate file extensions. </summary>
|
||||
|
|
@ -87,6 +90,7 @@ public class ModBackup
|
|||
try
|
||||
{
|
||||
Delete();
|
||||
_modExport.IgnoreExportedFile(Name);
|
||||
ArchiveUtility.CreateFromDirectory(_mod.ModPath.FullName, Name);
|
||||
Penumbra.Log.Debug($"Created export file {Name} from {_mod.ModPath.FullName}.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,17 +9,19 @@ public class ModExportManager : IDisposable, Luna.IService
|
|||
private readonly Configuration _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly FileWatcher _fileWatcher;
|
||||
|
||||
private DirectoryInfo? _exportDirectory;
|
||||
|
||||
public DirectoryInfo ExportDirectory
|
||||
=> _exportDirectory ?? _modManager.BasePath;
|
||||
|
||||
public ModExportManager(Configuration config, CommunicatorService communicator, ModManager modManager)
|
||||
public ModExportManager(Configuration config, CommunicatorService communicator, ModManager modManager, FileWatcher fileWatcher)
|
||||
{
|
||||
_config = config;
|
||||
_communicator = communicator;
|
||||
_modManager = modManager;
|
||||
_fileWatcher = fileWatcher;
|
||||
UpdateExportDirectory(_config.ExportDirectory, false);
|
||||
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModExportManager);
|
||||
}
|
||||
|
|
@ -34,6 +36,12 @@ public class ModExportManager : IDisposable, Luna.IService
|
|||
return backup.CreateAsync();
|
||||
}
|
||||
|
||||
public void IgnoreExportedFile(string fullPath)
|
||||
{
|
||||
if (_config.PreventExportLoopback)
|
||||
_fileWatcher.IgnoreFile(fullPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the export directory to a new directory. Can also reset it to null with empty input.
|
||||
/// If the directory is changed, all existing backups will be moved to the new one.
|
||||
|
|
|
|||
|
|
@ -6,16 +6,20 @@ namespace Penumbra.Services;
|
|||
|
||||
public sealed class FileWatcher : IDisposable, IService
|
||||
{
|
||||
private readonly ConcurrentSet<string> _pending = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ModImportManager _modImportManager;
|
||||
private readonly MessageService _messageService;
|
||||
private readonly Configuration _config;
|
||||
private readonly ConcurrentSet<string> _pending = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, long> _ignored = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ModImportManager _modImportManager;
|
||||
private readonly MessageService _messageService;
|
||||
private readonly Configuration _config;
|
||||
|
||||
private bool _pausedConsumer;
|
||||
private FileSystemWatcher? _fsw;
|
||||
private CancellationTokenSource? _cts = new();
|
||||
private Task? _consumer;
|
||||
|
||||
/// <summary> The time-to-live of ignore entries, in the same unit as <see cref="Environment.TickCount64"/>, namely milliseconds. </summary>
|
||||
private const long IgnoreTimeToLive = 60000L;
|
||||
|
||||
public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config)
|
||||
{
|
||||
_modImportManager = modImportManager;
|
||||
|
|
@ -48,6 +52,12 @@ public sealed class FileWatcher : IDisposable, IService
|
|||
}
|
||||
}
|
||||
|
||||
public void IgnoreFile(string fullPath)
|
||||
{
|
||||
if (_config.EnableDirectoryWatch)
|
||||
_ignored[fullPath] = Environment.TickCount64 + IgnoreTimeToLive;
|
||||
}
|
||||
|
||||
private void EndFileWatcher()
|
||||
{
|
||||
if (_fsw is null)
|
||||
|
|
@ -123,12 +133,16 @@ public sealed class FileWatcher : IDisposable, IService
|
|||
}
|
||||
|
||||
private void OnPath(object? sender, FileSystemEventArgs e)
|
||||
=> _pending.TryAdd(e.FullPath);
|
||||
{
|
||||
if (!_ignored.TryRemove(e.FullPath, out var expiresAtTickCount) || expiresAtTickCount <= Environment.TickCount64)
|
||||
_pending.TryAdd(e.FullPath);
|
||||
}
|
||||
|
||||
private async Task ConsumerLoopAsync(CancellationToken token)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
GarbageCollectIgnored();
|
||||
var path = _pending.FirstOrDefault<string>();
|
||||
if (path is null || _pausedConsumer)
|
||||
{
|
||||
|
|
@ -155,6 +169,15 @@ public sealed class FileWatcher : IDisposable, IService
|
|||
}
|
||||
}
|
||||
|
||||
private void GarbageCollectIgnored()
|
||||
{
|
||||
foreach (var entry in _ignored)
|
||||
{
|
||||
if (Environment.TickCount64 >= entry.Value)
|
||||
_ignored.TryRemove(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessOneAsync(string path, CancellationToken token)
|
||||
{
|
||||
// Downloads often finish via rename; file may be locked briefly.
|
||||
|
|
@ -245,6 +268,15 @@ public sealed class FileWatcher : IDisposable, IService
|
|||
|
||||
table.DrawColumn("Pending Files"u8);
|
||||
table.DrawColumn(StringU8.Join('\n', fileWatcher._pending));
|
||||
|
||||
table.DrawColumn("Ignored Files"u8);
|
||||
// FIXME .ToList() forces the use of an IReadOnlyCollection overload because, at the time of writing, IEnumerable ones don't handle empty enumerables correctly.
|
||||
table.DrawColumn(StringU8.Join((byte)'\n', fileWatcher._ignored.Select(entry =>
|
||||
(entry.Value - Environment.TickCount64) switch
|
||||
{
|
||||
<= 0 => $"<EXPIRED> {entry.Key}",
|
||||
var ttl => $"<{ttl}ms> {entry.Key}",
|
||||
}).ToList()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,7 +190,9 @@ public class PcpService : IApiService, IDisposable
|
|||
var modDirectory = CreateMod(identifier, note, time);
|
||||
await CreateDefaultMod(modDirectory, meta, tree, cancel);
|
||||
await CreateCollectionInfo(modDirectory, objectIndex, identifier, note, time, cancel);
|
||||
var file = ZipUp(modDirectory, modPath, Extension);
|
||||
var file = GetFullZipPath(modDirectory, modPath, Extension);
|
||||
_modExport.IgnoreExportedFile(file);
|
||||
ZipUp(modDirectory, file);
|
||||
return (true, file);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -199,15 +201,19 @@ public class PcpService : IApiService, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private static string ZipUp(DirectoryInfo directory, string? path, string extension)
|
||||
private static string GetFullZipPath(DirectoryInfo directory, string? path, string extension)
|
||||
{
|
||||
if (path is null)
|
||||
path = directory.FullName + extension;
|
||||
else if (Path.GetExtension(path.AsSpan()).IsEmpty)
|
||||
path += extension;
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void ZipUp(DirectoryInfo directory, string path)
|
||||
{
|
||||
ArchiveUtility.CreateFromDirectory(directory.FullName, path);
|
||||
directory.Delete(true);
|
||||
return path;
|
||||
}
|
||||
|
||||
private async Task CreateCollectionInfo(DirectoryInfo directory, ObjectIndex index, ActorIdentifier actor, string note, DateTime time,
|
||||
|
|
|
|||
|
|
@ -639,6 +639,9 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
Checkbox("Enable Fully Automatic Import"u8,
|
||||
"Uses the File Watcher in order to skip the query popup and automatically import any new mods."u8,
|
||||
_config.EnableAutomaticModImport, v => _config.EnableAutomaticModImport = v);
|
||||
Checkbox("Prevent Exported Mods From Being Automatically Reimported"u8,
|
||||
"If your Automatic Import Directory is the same as your Default Mod Export Directory, prevents mods and character packs you export from being reimported or showing a query popup."u8,
|
||||
_config.PreventExportLoopback, v => _config.PreventExportLoopback = v);
|
||||
DrawFileWatcherPath();
|
||||
}
|
||||
|
||||
|
|
@ -738,7 +741,7 @@ public sealed class SettingsTab : ITab<TabType>
|
|||
}
|
||||
|
||||
style.Pop();
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Automatic Import Director"u8,
|
||||
LunaStyle.DrawAlignedHelpMarkerLabel("Automatic Import Directory"u8,
|
||||
"Choose the Directory the File Watcher listens to."u8);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue