Cut over the window to Explorer.Host.exe so only the host writes the index.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using Explorer.Hosting;
|
||||
using Explorer.Hosting.Ipc;
|
||||
using Explorer.Presentation.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@@ -10,6 +12,7 @@ namespace Explorer.App;
|
||||
public partial class App : System.Windows.Application
|
||||
{
|
||||
private IHost? _host;
|
||||
private WorkbenchPipeClient? _workbenchClient;
|
||||
|
||||
protected override async void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
@@ -29,9 +32,35 @@ public partial class App : System.Windows.Application
|
||||
retainedFileCountLimit: 14)
|
||||
.CreateLogger();
|
||||
|
||||
_workbenchClient = await WorkbenchHostConnector.ConnectOrStartAsync(
|
||||
TimeSpan.FromSeconds(12),
|
||||
logger: null).ConfigureAwait(true);
|
||||
var hostExe = HostLogonAutostart.FindHostExecutable();
|
||||
if (_workbenchClient is null && hostExe is not null)
|
||||
{
|
||||
MessageBox.Show(
|
||||
"Explorer.Host.exe is present but the window could not connect to it. See logs.",
|
||||
"Explorer Workbench",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
Shutdown(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
_host = Host.CreateDefaultBuilder()
|
||||
.UseSerilog()
|
||||
.ConfigureServices((_, services) => services.AddExplorer())
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
if (_workbenchClient is not null)
|
||||
{
|
||||
services.AddExplorerClient(_workbenchClient);
|
||||
services.AddExplorerUi();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddExplorer();
|
||||
}
|
||||
})
|
||||
.Build();
|
||||
|
||||
var vm = _host.Services.GetRequiredService<MainViewModel>();
|
||||
@@ -60,6 +89,11 @@ public partial class App : System.Windows.Application
|
||||
_host.Dispose();
|
||||
}
|
||||
|
||||
if (_workbenchClient is not null)
|
||||
{
|
||||
await _workbenchClient.DisposeAsync().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
Log.CloseAndFlush();
|
||||
base.OnExit(e);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<CheckBox x:Name="BackgroundHostAtLogon" Margin="0,0,0,6"
|
||||
Content="Start Explorer.Host.exe at Windows sign-in"/>
|
||||
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="24,0,0,18" FontSize="12"
|
||||
Text="Registers a per-user logon task. The window still owns indexing in this version. If the host starts while this window is open, it exits because the index is already in use."/>
|
||||
Text="Registers a per-user logon task. The window connects to Explorer.Host.exe for indexing and the queue. If the host is not running, the window starts it. Only the host opens the index for write."/>
|
||||
|
||||
<TextBlock Text="7-Zip" FontSize="16" FontWeight="SemiBold" Margin="0,8,0,10"/>
|
||||
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgMuted}" Margin="0,0,0,8" FontSize="12"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using Explorer.Contracts;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -12,6 +13,7 @@ public sealed class SourceManager
|
||||
private readonly IAppEnvironment _env;
|
||||
private readonly IClock _clock;
|
||||
private readonly ILogger<SourceManager> _logger;
|
||||
private readonly ISourceHost? _remote;
|
||||
|
||||
private readonly object _refreshLock = new();
|
||||
private Task<IReadOnlyList<Source>>? _refreshInFlight;
|
||||
@@ -23,18 +25,31 @@ public sealed class SourceManager
|
||||
IVolumeService volumes,
|
||||
IAppEnvironment env,
|
||||
IClock clock,
|
||||
ILogger<SourceManager> logger)
|
||||
ILogger<SourceManager> logger,
|
||||
ISourceHost? remote = null)
|
||||
{
|
||||
_store = store;
|
||||
_volumes = volumes;
|
||||
_env = env;
|
||||
_clock = clock;
|
||||
_logger = logger;
|
||||
_remote = remote;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _store.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!_store.CanWrite)
|
||||
{
|
||||
if (_remote is not null)
|
||||
{
|
||||
await _remote.RefreshAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
InvalidateRefreshCache();
|
||||
return;
|
||||
}
|
||||
|
||||
await _store.ScanJobs.InterruptRunningAsync(cancellationToken).ConfigureAwait(false);
|
||||
await _store.Excludes.EnsureDefaultsAsync(DefaultExcludes.Create(), cancellationToken).ConfigureAwait(false);
|
||||
await RefreshOnlineStateAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -70,6 +85,22 @@ public sealed class SourceManager
|
||||
|
||||
private async Task<IReadOnlyList<Source>> RefreshOnlineStateCoreAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_store.CanWrite)
|
||||
{
|
||||
if (_remote is not null)
|
||||
{
|
||||
await _remote.RefreshAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var snapshot = await _store.Sources.GetAllAsync(cancellationToken).ConfigureAwait(false);
|
||||
lock (_refreshLock)
|
||||
{
|
||||
_refreshCacheTimestamp = Stopwatch.GetTimestamp();
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
var started = Stopwatch.GetTimestamp();
|
||||
var known = (await _store.Sources.GetAllAsync(cancellationToken).ConfigureAwait(false)).ToList();
|
||||
var online = _volumes.EnumerateOnlineVolumes();
|
||||
@@ -201,6 +232,18 @@ public sealed class SourceManager
|
||||
|
||||
public async Task<Source> AddUncAsync(string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_store.CanWrite)
|
||||
{
|
||||
if (_remote is null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot add a network location while the index is read-only.");
|
||||
}
|
||||
|
||||
var added = await _remote.AddUncAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
InvalidateRefreshCache();
|
||||
return added;
|
||||
}
|
||||
|
||||
var root = PathRules.CanonicalUncRoot(path);
|
||||
var known = await _store.Sources.GetAllAsync(cancellationToken).ConfigureAwait(false);
|
||||
var fp = new VolumeFingerprint
|
||||
@@ -269,6 +312,18 @@ public sealed class SourceManager
|
||||
|
||||
public async Task<bool> ForgetDisconnectedAsync(string path, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_store.CanWrite)
|
||||
{
|
||||
if (_remote is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var forgotten = await _remote.ForgetAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
InvalidateRefreshCache();
|
||||
return forgotten;
|
||||
}
|
||||
|
||||
var source = await FindSourceRootAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
if (source is null || !CanForget(source))
|
||||
{
|
||||
@@ -324,6 +379,18 @@ public sealed class SourceManager
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!_store.CanWrite)
|
||||
{
|
||||
if (_remote is null)
|
||||
{
|
||||
return await FindByPathAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var ensured = await _remote.EnsureForPathAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
InvalidateRefreshCache();
|
||||
return ensured;
|
||||
}
|
||||
|
||||
var existing = await FindByPathAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
if (existing is not null)
|
||||
{
|
||||
|
||||
@@ -1,15 +1,58 @@
|
||||
using Explorer.Contracts;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
|
||||
namespace Explorer.Application;
|
||||
|
||||
public sealed class WorkbenchHost : IWorkbenchHost
|
||||
{
|
||||
public WorkbenchHost(IIndexingHost indexing, ITransferHost transfers)
|
||||
public WorkbenchHost(IIndexingHost indexing, ITransferHost transfers, ISourceHost sources, IIndexMutations mutations)
|
||||
{
|
||||
Indexing = indexing;
|
||||
Transfers = transfers;
|
||||
Sources = sources;
|
||||
Mutations = mutations;
|
||||
}
|
||||
|
||||
public IIndexingHost Indexing { get; }
|
||||
public ITransferHost Transfers { get; }
|
||||
public ISourceHost Sources { get; }
|
||||
public IIndexMutations Mutations { get; }
|
||||
}
|
||||
|
||||
public sealed class LocalSourceHost : ISourceHost
|
||||
{
|
||||
private readonly SourceManager _sources;
|
||||
public LocalSourceHost(SourceManager sources) => _sources = sources;
|
||||
public Task RefreshAsync(CancellationToken cancellationToken = default)
|
||||
=> _sources.RefreshOnlineStateAsync(forceRefresh: true, cancellationToken);
|
||||
public Task<Source> AddUncAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> _sources.AddUncAsync(path, cancellationToken);
|
||||
public Task<Source?> EnsureForPathAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> _sources.EnsureForPathAsync(path, cancellationToken);
|
||||
public Task<bool> ForgetAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> _sources.ForgetDisconnectedAsync(path, cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class LocalIndexMutations : IIndexMutations
|
||||
{
|
||||
private readonly IIndexStore _store;
|
||||
public LocalIndexMutations(IIndexStore store) => _store = store;
|
||||
|
||||
public Task<long> UpsertSyncProfileAsync(SyncProfile profile, CancellationToken cancellationToken = default)
|
||||
=> _store.SyncProfiles.UpsertAsync(profile, cancellationToken);
|
||||
public Task DeleteSyncProfileAsync(long id, CancellationToken cancellationToken = default)
|
||||
=> _store.SyncProfiles.DeleteAsync(id, cancellationToken);
|
||||
public Task<long> UpsertOperationProfileAsync(OperationProfile profile, CancellationToken cancellationToken = default)
|
||||
=> _store.OperationProfiles.UpsertAsync(profile, cancellationToken);
|
||||
public Task DeleteOperationProfileAsync(long id, CancellationToken cancellationToken = default)
|
||||
=> _store.OperationProfiles.DeleteAsync(id, cancellationToken);
|
||||
public Task<long> CreateRenameBatchAsync(IReadOnlyList<RenameBatchItem> items, CancellationToken cancellationToken = default)
|
||||
=> _store.RenameBatches.CreateAsync(items, cancellationToken);
|
||||
public Task MarkRenameBatchUndoneAsync(long id, CancellationToken cancellationToken = default)
|
||||
=> _store.RenameBatches.MarkUndoneAsync(id, cancellationToken);
|
||||
public Task EnqueueHashCollisionsAsync(long? sourceId, CancellationToken cancellationToken = default)
|
||||
=> _store.Hashes.EnqueueSizeCollisionsAsync(sourceId, cancellationToken);
|
||||
public Task UpsertRelationAsync(FileRelation relation, CancellationToken cancellationToken = default)
|
||||
=> _store.Relations.UpsertAsync(relation, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ public interface IWorkbenchHost
|
||||
{
|
||||
IIndexingHost Indexing { get; }
|
||||
ITransferHost Transfers { get; }
|
||||
ISourceHost Sources { get; }
|
||||
IIndexMutations Mutations { get; }
|
||||
}
|
||||
|
||||
public interface IIndexingHost
|
||||
@@ -33,4 +35,42 @@ public interface ITransferHost
|
||||
void ClearFinished();
|
||||
bool MoveUp(long jobId);
|
||||
bool MoveDown(long jobId);
|
||||
Task EnqueueCopyAsync(IReadOnlyList<string> sources, string destinationDirectory, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
Task EnqueueMoveAsync(IReadOnlyList<string> sources, string destinationDirectory, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
Task EnqueueDeleteAsync(IReadOnlyList<string> paths, bool permanent = false, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
Task EnqueueRenameAsync(string path, string newName, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
Task EnqueueEmptyRecycleBinAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
Task EnqueueExtractAsync(string archivePath, string destinationDirectory, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
Task EnqueueCompressAsync(IReadOnlyList<string> sources, string archivePath, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
Task EnqueueAddToArchiveAsync(string archivePath, IReadOnlyList<string> sources, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
Task EnqueueVerifyArchiveAsync(string archivePath, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
public interface ISourceHost
|
||||
{
|
||||
Task RefreshAsync(CancellationToken cancellationToken = default);
|
||||
Task<Source> AddUncAsync(string path, CancellationToken cancellationToken = default);
|
||||
Task<Source?> EnsureForPathAsync(string path, CancellationToken cancellationToken = default);
|
||||
Task<bool> ForgetAsync(string path, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IIndexMutations
|
||||
{
|
||||
Task<long> UpsertSyncProfileAsync(SyncProfile profile, CancellationToken cancellationToken = default);
|
||||
Task DeleteSyncProfileAsync(long id, CancellationToken cancellationToken = default);
|
||||
Task<long> UpsertOperationProfileAsync(OperationProfile profile, CancellationToken cancellationToken = default);
|
||||
Task DeleteOperationProfileAsync(long id, CancellationToken cancellationToken = default);
|
||||
Task<long> CreateRenameBatchAsync(IReadOnlyList<RenameBatchItem> items, CancellationToken cancellationToken = default);
|
||||
Task MarkRenameBatchUndoneAsync(long id, CancellationToken cancellationToken = default);
|
||||
Task EnqueueHashCollisionsAsync(long? sourceId, CancellationToken cancellationToken = default);
|
||||
Task UpsertRelationAsync(FileRelation relation, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ public interface IIndexStore
|
||||
ISyncProfileStore SyncProfiles { get; }
|
||||
IOperationProfileStore OperationProfiles { get; }
|
||||
|
||||
bool CanWrite { get; }
|
||||
|
||||
Task RunWriteAsync(Func<IIndexStore, Task> work, CancellationToken cancellationToken = default);
|
||||
Task<T> RunWriteAsync<T>(Func<IIndexStore, Task<T>> work, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using Explorer.Contracts;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
using Explorer.FileOperations;
|
||||
|
||||
namespace Explorer.FileOperations;
|
||||
|
||||
public sealed class FileOperationService
|
||||
{
|
||||
private readonly TransferQueue _queue;
|
||||
private readonly ITransferHost _queue;
|
||||
private readonly IShellFileOperations _shell;
|
||||
private readonly IFileSystemEnumerator _enumerator;
|
||||
|
||||
public FileOperationService(TransferQueue queue, IShellFileOperations shell, IFileSystemEnumerator enumerator)
|
||||
public FileOperationService(ITransferHost queue, IShellFileOperations shell, IFileSystemEnumerator enumerator)
|
||||
{
|
||||
_queue = queue;
|
||||
_shell = shell;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Explorer.Application;
|
||||
using Explorer.Contracts;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
|
||||
@@ -9,6 +10,7 @@ public sealed class FolderSyncService
|
||||
{
|
||||
private readonly FolderSyncPlanner _planner;
|
||||
private readonly IIndexStore _store;
|
||||
private readonly IIndexMutations _mutations;
|
||||
private readonly SourceManager _sources;
|
||||
private readonly FileOperationService _ops;
|
||||
private readonly IVolumeService _volumes;
|
||||
@@ -20,6 +22,7 @@ public sealed class FolderSyncService
|
||||
public FolderSyncService(
|
||||
FolderSyncPlanner planner,
|
||||
IIndexStore store,
|
||||
IIndexMutations mutations,
|
||||
SourceManager sources,
|
||||
FileOperationService ops,
|
||||
IVolumeService volumes,
|
||||
@@ -28,6 +31,7 @@ public sealed class FolderSyncService
|
||||
{
|
||||
_planner = planner;
|
||||
_store = store;
|
||||
_mutations = mutations;
|
||||
_sources = sources;
|
||||
_ops = ops;
|
||||
_volumes = volumes;
|
||||
@@ -46,12 +50,12 @@ public sealed class FolderSyncService
|
||||
}
|
||||
|
||||
AttachVolumeGuids(profile);
|
||||
profile.Id = await _store.SyncProfiles.UpsertAsync(profile, cancellationToken).ConfigureAwait(false);
|
||||
profile.Id = await _mutations.UpsertSyncProfileAsync(profile, cancellationToken).ConfigureAwait(false);
|
||||
return profile.Id;
|
||||
}
|
||||
|
||||
public Task DeleteAsync(long id, CancellationToken cancellationToken = default)
|
||||
=> _store.SyncProfiles.DeleteAsync(id, cancellationToken);
|
||||
=> _mutations.DeleteSyncProfileAsync(id, cancellationToken);
|
||||
|
||||
public async Task<OperationPlan> PreviewAsync(SyncProfile profile, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -97,7 +101,7 @@ public sealed class FolderSyncService
|
||||
profile.LastStatus = deletes > 0
|
||||
? $"Queued {copies} copy, {deletes} delete"
|
||||
: $"Queued {copies} copy";
|
||||
await _store.SyncProfiles.UpsertAsync(profile, cancellationToken).ConfigureAwait(false);
|
||||
await _mutations.UpsertSyncProfileAsync(profile, cancellationToken).ConfigureAwait(false);
|
||||
return plan;
|
||||
}
|
||||
|
||||
@@ -171,7 +175,7 @@ public sealed class FolderSyncService
|
||||
return;
|
||||
}
|
||||
|
||||
await _store.Relations.UpsertAsync(new FileRelation
|
||||
await _mutations.UpsertRelationAsync(new FileRelation
|
||||
{
|
||||
LeftEntryId = left.Id,
|
||||
RightEntryId = right.Id,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Explorer.Application;
|
||||
using Explorer.Contracts;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
|
||||
@@ -9,6 +10,7 @@ public sealed class OperationProfileService
|
||||
{
|
||||
private readonly FileOperationProfilePlanner _planner;
|
||||
private readonly IIndexStore _store;
|
||||
private readonly IIndexMutations _mutations;
|
||||
private readonly FileOperationService _ops;
|
||||
private readonly RenameBatchService _renames;
|
||||
private readonly IVolumeService _volumes;
|
||||
@@ -21,6 +23,7 @@ public sealed class OperationProfileService
|
||||
public OperationProfileService(
|
||||
FileOperationProfilePlanner planner,
|
||||
IIndexStore store,
|
||||
IIndexMutations mutations,
|
||||
FileOperationService ops,
|
||||
RenameBatchService renames,
|
||||
IVolumeService volumes,
|
||||
@@ -31,6 +34,7 @@ public sealed class OperationProfileService
|
||||
{
|
||||
_planner = planner;
|
||||
_store = store;
|
||||
_mutations = mutations;
|
||||
_ops = ops;
|
||||
_renames = renames;
|
||||
_volumes = volumes;
|
||||
@@ -55,12 +59,12 @@ public sealed class OperationProfileService
|
||||
|
||||
AttachVolumeGuids(profile);
|
||||
profile.AutoRun = profile.CanAutoRun;
|
||||
profile.Id = await _store.OperationProfiles.UpsertAsync(profile, cancellationToken).ConfigureAwait(false);
|
||||
profile.Id = await _mutations.UpsertOperationProfileAsync(profile, cancellationToken).ConfigureAwait(false);
|
||||
return profile.Id;
|
||||
}
|
||||
|
||||
public Task DeleteAsync(long id, CancellationToken cancellationToken = default)
|
||||
=> _store.OperationProfiles.DeleteAsync(id, cancellationToken);
|
||||
=> _mutations.DeleteOperationProfileAsync(id, cancellationToken);
|
||||
|
||||
public async Task<OperationProfile> DuplicateAsync(OperationProfile profile, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -150,7 +154,7 @@ public sealed class OperationProfileService
|
||||
var renamed = renames.Count;
|
||||
profile.LastRunUtc = DateTimeOffset.UtcNow;
|
||||
profile.LastStatus = $"Queued {copies} copy, {compress} compress, {renamed} rename";
|
||||
await _store.OperationProfiles.UpsertAsync(profile, cancellationToken).ConfigureAwait(false);
|
||||
await _mutations.UpsertOperationProfileAsync(profile, cancellationToken).ConfigureAwait(false);
|
||||
return plan;
|
||||
}
|
||||
|
||||
@@ -206,7 +210,7 @@ public sealed class OperationProfileService
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
await _store.OperationProfiles.UpsertAsync(new OperationProfile
|
||||
await _mutations.UpsertOperationProfileAsync(new OperationProfile
|
||||
{
|
||||
Name = "Archive folder",
|
||||
RequireGitClean = true,
|
||||
@@ -216,7 +220,7 @@ public sealed class OperationProfileService
|
||||
IsBuiltIn = true,
|
||||
CreatedUtc = now
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
await _store.OperationProfiles.UpsertAsync(new OperationProfile
|
||||
await _mutations.UpsertOperationProfileAsync(new OperationProfile
|
||||
{
|
||||
Name = "Copy to destination",
|
||||
DoCopy = true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Explorer.Application;
|
||||
using Explorer.Contracts;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
|
||||
@@ -8,12 +9,14 @@ public sealed class RenameBatchService
|
||||
{
|
||||
private readonly RenamePlanner _planner;
|
||||
private readonly IIndexStore _store;
|
||||
private readonly IIndexMutations _mutations;
|
||||
private readonly FileOperationService _ops;
|
||||
|
||||
public RenameBatchService(RenamePlanner planner, IIndexStore store, FileOperationService ops)
|
||||
public RenameBatchService(RenamePlanner planner, IIndexStore store, IIndexMutations mutations, FileOperationService ops)
|
||||
{
|
||||
_planner = planner;
|
||||
_store = store;
|
||||
_mutations = mutations;
|
||||
_ops = ops;
|
||||
}
|
||||
|
||||
@@ -36,7 +39,7 @@ public sealed class RenameBatchService
|
||||
var items = plan.Operations
|
||||
.Select((op, i) => new RenameBatchItem(op.SourcePath, op.DestinationPath ?? op.SourcePath, i))
|
||||
.ToList();
|
||||
var batchId = await _store.RenameBatches.CreateAsync(items, cancellationToken).ConfigureAwait(false);
|
||||
var batchId = await _mutations.CreateRenameBatchAsync(items, cancellationToken).ConfigureAwait(false);
|
||||
await _ops.EnqueueRenameAsync(
|
||||
plan.Operations.Select(op => (op.SourcePath, op.NewName ?? PathRules.GetFileName(op.DestinationPath!))).ToList(),
|
||||
cancellationToken)
|
||||
@@ -63,7 +66,7 @@ public sealed class RenameBatchService
|
||||
{
|
||||
if (!plan.HasErrors)
|
||||
{
|
||||
await _store.RenameBatches.MarkUndoneAsync(batch.Id, cancellationToken).ConfigureAwait(false);
|
||||
await _mutations.MarkRenameBatchUndoneAsync(batch.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return plan;
|
||||
@@ -73,7 +76,7 @@ public sealed class RenameBatchService
|
||||
plan.Operations.Select(op => (op.SourcePath, op.NewName ?? PathRules.GetFileName(op.DestinationPath!))).ToList(),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await _store.RenameBatches.MarkUndoneAsync(batch.Id, cancellationToken).ConfigureAwait(false);
|
||||
await _mutations.MarkRenameBatchUndoneAsync(batch.Id, cancellationToken).ConfigureAwait(false);
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,14 @@ namespace Explorer.Hosting;
|
||||
public static class ExplorerHostServices
|
||||
{
|
||||
public static IServiceCollection AddExplorerCore(this IServiceCollection services)
|
||||
{
|
||||
services.AddExplorerShared(readOnly: false);
|
||||
services.AddExplorerWorkers();
|
||||
services.AddExplorerOperations();
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddExplorerShared(this IServiceCollection services, bool readOnly)
|
||||
{
|
||||
services.TryAddSingleton<IClock, SystemClock>();
|
||||
services.TryAddSingleton<IAppEnvironment, WindowsAppEnvironment>();
|
||||
@@ -34,7 +42,7 @@ public static class ExplorerHostServices
|
||||
{
|
||||
var env = sp.GetRequiredService<IAppEnvironment>();
|
||||
var logger = sp.GetRequiredService<ILogger<SqliteIndexStore>>();
|
||||
return new SqliteIndexStore(env.DatabasePath, logger);
|
||||
return new SqliteIndexStore(env.DatabasePath, logger, readOnly);
|
||||
});
|
||||
services.AddSingleton<IStorageProvider, OneDriveStorageProvider>();
|
||||
services.AddSingleton<IStorageProvider, GoogleDriveStorageProvider>();
|
||||
@@ -47,7 +55,13 @@ public static class ExplorerHostServices
|
||||
services.AddSingleton<IGitCommandProvider>(sp => sp.GetRequiredService<WindowsGitStatusProvider>());
|
||||
services.AddSingleton<IElevatedScanService, WindowsElevatedScanService>();
|
||||
services.AddSingleton<IRecycleBinCatalog, WindowsRecycleBinCatalog>();
|
||||
services.AddSingleton<SourceManager>();
|
||||
services.AddSingleton(sp => new SourceManager(
|
||||
sp.GetRequiredService<IIndexStore>(),
|
||||
sp.GetRequiredService<IVolumeService>(),
|
||||
sp.GetRequiredService<IAppEnvironment>(),
|
||||
sp.GetRequiredService<IClock>(),
|
||||
sp.GetRequiredService<ILogger<SourceManager>>(),
|
||||
sp.GetService<ISourceHost>()));
|
||||
services.AddSingleton<PathHistoryStore>();
|
||||
services.AddSingleton<CloudPlaceStore>();
|
||||
services.AddSingleton<UiPreferencesStore>();
|
||||
@@ -57,24 +71,26 @@ public static class ExplorerHostServices
|
||||
services.AddSingleton<FilesystemScanner>();
|
||||
services.AddSingleton<FolderReconciler>();
|
||||
services.AddSingleton<UsnChangeApplier>();
|
||||
services.AddSingleton<IndexingCoordinator>();
|
||||
services.AddSingleton<DirectoryWatcherHub>();
|
||||
services.AddSingleton<SearchService>();
|
||||
services.AddSingleton<AnalysisService>();
|
||||
services.AddSingleton<RenamePlanner>();
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddExplorerWorkers(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IndexingCoordinator>();
|
||||
services.AddSingleton<DirectoryWatcherHub>();
|
||||
services.AddSingleton<IOperationExecutor, NativeFileOperationExecutor>();
|
||||
services.AddSingleton<TransferQueue>();
|
||||
services.AddSingleton<ITransferHost>(sp => sp.GetRequiredService<TransferQueue>());
|
||||
services.AddSingleton<IIndexingHost>(sp => sp.GetRequiredService<IndexingCoordinator>());
|
||||
services.AddSingleton<IWorkbenchHost, WorkbenchHost>();
|
||||
services.AddSingleton<FileOperationService>();
|
||||
services.AddSingleton<RenamePlanner>();
|
||||
services.AddSingleton<RenameBatchService>();
|
||||
services.AddSingleton<FolderSyncPlanner>();
|
||||
services.AddSingleton<FolderSyncService>();
|
||||
services.AddSingleton<FileOperationProfilePlanner>();
|
||||
services.AddSingleton<OperationProfileService>();
|
||||
services.AddSingleton<ReorganizePlanner>();
|
||||
services.AddSingleton<ReorganizeService>();
|
||||
services.AddSingleton<IIndexMutations>(sp => new LocalIndexMutations(sp.GetRequiredService<IIndexStore>()));
|
||||
services.AddSingleton<IWorkbenchHost>(sp => new WorkbenchHost(
|
||||
sp.GetRequiredService<IIndexingHost>(),
|
||||
sp.GetRequiredService<ITransferHost>(),
|
||||
new LocalSourceHost(sp.GetRequiredService<SourceManager>()),
|
||||
sp.GetRequiredService<IIndexMutations>()));
|
||||
services.AddSingleton<DuplicateHashWorker>();
|
||||
services.AddSingleton<HistoryRollupService>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<IndexingCoordinator>());
|
||||
@@ -85,10 +101,32 @@ public static class ExplorerHostServices
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the background host process: open the store first, then Core workers, then the named pipe.
|
||||
/// Do not call this from the GUI while the GUI still opens the index for write.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddExplorerOperations(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<FileOperationService>();
|
||||
services.AddSingleton<RenameBatchService>();
|
||||
services.AddSingleton<FolderSyncPlanner>();
|
||||
services.AddSingleton<FolderSyncService>();
|
||||
services.AddSingleton<FileOperationProfilePlanner>();
|
||||
services.AddSingleton<OperationProfileService>();
|
||||
services.AddSingleton<ReorganizePlanner>();
|
||||
services.AddSingleton<ReorganizeService>();
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddExplorerClient(this IServiceCollection services, IWorkbenchHost workbench)
|
||||
{
|
||||
services.AddSingleton(workbench);
|
||||
services.AddSingleton(workbench.Indexing);
|
||||
services.AddSingleton(workbench.Transfers);
|
||||
services.AddSingleton(workbench.Sources);
|
||||
services.AddSingleton(workbench.Mutations);
|
||||
services.AddExplorerShared(readOnly: true);
|
||||
services.AddExplorerOperations();
|
||||
services.AddHostedService<IndexStoreLifetime>();
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddExplorerHostProcess(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<WorkbenchIpcOptions>();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Explorer.Application;
|
||||
using Explorer.Domain.Abstractions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
@@ -6,10 +7,19 @@ namespace Explorer.Hosting;
|
||||
public sealed class IndexStoreLifetime : IHostedService
|
||||
{
|
||||
private readonly IIndexStore _store;
|
||||
private readonly SourceManager _sources;
|
||||
|
||||
public IndexStoreLifetime(IIndexStore store) => _store = store;
|
||||
public IndexStoreLifetime(IIndexStore store, SourceManager sources)
|
||||
{
|
||||
_store = store;
|
||||
_sources = sources;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => _store.OpenAsync(cancellationToken);
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _store.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
await _sources.InitializeAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => _store.CloseAsync();
|
||||
}
|
||||
|
||||
@@ -42,7 +42,11 @@ internal sealed class IpcEnvelope
|
||||
public string? S { get; set; }
|
||||
public bool? Flag { get; set; }
|
||||
public bool? Paused { get; set; }
|
||||
public string? Dest { get; set; }
|
||||
public string[]? Paths { get; set; }
|
||||
public string? Payload { get; set; }
|
||||
public ScanProgress? Progress { get; set; }
|
||||
public TransferJob? Job { get; set; }
|
||||
public TransferJob[]? Jobs { get; set; }
|
||||
public Source? Source { get; set; }
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ public sealed class WorkbenchPipeClient : IWorkbenchHost, IAsyncDisposable
|
||||
private readonly Task _readLoop;
|
||||
private readonly IndexingProxy _indexing;
|
||||
private readonly TransferProxy _transfers;
|
||||
private readonly SourceProxy _sources;
|
||||
private readonly MutationProxy _mutations;
|
||||
|
||||
private WorkbenchPipeClient(NamedPipeClientStream pipe)
|
||||
{
|
||||
@@ -26,11 +28,15 @@ public sealed class WorkbenchPipeClient : IWorkbenchHost, IAsyncDisposable
|
||||
_reader = new StreamReader(pipe, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
|
||||
_indexing = new IndexingProxy(this);
|
||||
_transfers = new TransferProxy(this);
|
||||
_sources = new SourceProxy(this);
|
||||
_mutations = new MutationProxy(this);
|
||||
_readLoop = ReadLoopAsync(_cts.Token);
|
||||
}
|
||||
|
||||
public IIndexingHost Indexing => _indexing;
|
||||
public ITransferHost Transfers => _transfers;
|
||||
public ISourceHost Sources => _sources;
|
||||
public IIndexMutations Mutations => _mutations;
|
||||
|
||||
public static async Task<WorkbenchPipeClient> ConnectAsync(
|
||||
WorkbenchIpcOptions options,
|
||||
@@ -86,12 +92,27 @@ public sealed class WorkbenchPipeClient : IWorkbenchHost, IAsyncDisposable
|
||||
string op,
|
||||
CancellationToken cancellationToken,
|
||||
long? n = null,
|
||||
string? s = null)
|
||||
string? s = null,
|
||||
string? dest = null,
|
||||
string[]? paths = null,
|
||||
bool? flag = null,
|
||||
string? payload = null)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString("N");
|
||||
var tcs = new TaskCompletionSource<IpcEnvelope>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_pending[id] = tcs;
|
||||
var request = new IpcEnvelope { V = WorkbenchIpc.ProtocolVersion, Id = id, Op = op, N = n, S = s };
|
||||
var request = new IpcEnvelope
|
||||
{
|
||||
V = WorkbenchIpc.ProtocolVersion,
|
||||
Id = id,
|
||||
Op = op,
|
||||
N = n,
|
||||
S = s,
|
||||
Dest = dest,
|
||||
Paths = paths,
|
||||
Flag = flag,
|
||||
Payload = payload
|
||||
};
|
||||
var json = JsonSerializer.Serialize(request, WorkbenchIpc.Json);
|
||||
await _send.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
@@ -270,7 +291,69 @@ public sealed class WorkbenchPipeClient : IWorkbenchHost, IAsyncDisposable
|
||||
public void ClearFinished() => _client.Call("Transfers.ClearFinished");
|
||||
public bool MoveUp(long jobId) => _client.Call("Transfers.MoveUp", jobId).Flag == true;
|
||||
public bool MoveDown(long jobId) => _client.Call("Transfers.MoveDown", jobId).Flag == true;
|
||||
public Task EnqueueCopyAsync(IReadOnlyList<string> sources, string destinationDirectory, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Transfers.EnqueueCopy", cancellationToken, dest: destinationDirectory, paths: sources.ToArray());
|
||||
public Task EnqueueMoveAsync(IReadOnlyList<string> sources, string destinationDirectory, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Transfers.EnqueueMove", cancellationToken, dest: destinationDirectory, paths: sources.ToArray());
|
||||
public Task EnqueueDeleteAsync(IReadOnlyList<string> paths, bool permanent = false, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Transfers.EnqueueDelete", cancellationToken, paths: paths.ToArray(), flag: permanent);
|
||||
public Task EnqueueRenameAsync(string path, string newName, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Transfers.EnqueueRename", cancellationToken, s: path, dest: newName);
|
||||
public Task EnqueueEmptyRecycleBinAsync(CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Transfers.EnqueueEmptyRecycleBin", cancellationToken);
|
||||
public Task EnqueueExtractAsync(string archivePath, string destinationDirectory, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Transfers.EnqueueExtract", cancellationToken, s: archivePath, dest: destinationDirectory);
|
||||
public Task EnqueueCompressAsync(IReadOnlyList<string> sources, string archivePath, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Transfers.EnqueueCompress", cancellationToken, dest: archivePath, paths: sources.ToArray());
|
||||
public Task EnqueueAddToArchiveAsync(string archivePath, IReadOnlyList<string> sources, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Transfers.EnqueueAddToArchive", cancellationToken, dest: archivePath, paths: sources.ToArray());
|
||||
public Task EnqueueVerifyArchiveAsync(string archivePath, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Transfers.EnqueueVerifyArchive", cancellationToken, s: archivePath);
|
||||
public void RaiseChanged() => Changed?.Invoke(this, EventArgs.Empty);
|
||||
public void RaiseFinished(TransferJob job) => JobFinished?.Invoke(this, job);
|
||||
}
|
||||
|
||||
private sealed class SourceProxy : ISourceHost
|
||||
{
|
||||
private readonly WorkbenchPipeClient _client;
|
||||
public SourceProxy(WorkbenchPipeClient client) => _client = client;
|
||||
|
||||
public Task RefreshAsync(CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Sources.Refresh", cancellationToken);
|
||||
public async Task<Source> AddUncAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> (await _client.CallAsync("Sources.AddUnc", cancellationToken, s: path).ConfigureAwait(false)).Source
|
||||
?? throw new InvalidOperationException("Host did not return a source.");
|
||||
public Task<Source?> EnsureForPathAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> CallSource("Sources.EnsureForPath", path, cancellationToken);
|
||||
public async Task<bool> ForgetAsync(string path, CancellationToken cancellationToken = default)
|
||||
=> (await _client.CallAsync("Sources.Forget", cancellationToken, s: path).ConfigureAwait(false)).Flag == true;
|
||||
|
||||
private async Task<Source?> CallSource(string op, string path, CancellationToken cancellationToken)
|
||||
=> (await _client.CallAsync(op, cancellationToken, s: path).ConfigureAwait(false)).Source;
|
||||
}
|
||||
|
||||
private sealed class MutationProxy : IIndexMutations
|
||||
{
|
||||
private readonly WorkbenchPipeClient _client;
|
||||
public MutationProxy(WorkbenchPipeClient client) => _client = client;
|
||||
|
||||
public async Task<long> UpsertSyncProfileAsync(SyncProfile profile, CancellationToken cancellationToken = default)
|
||||
=> (await _client.CallAsync("Mutations.UpsertSyncProfile", cancellationToken, payload: Json(profile)).ConfigureAwait(false)).N ?? 0;
|
||||
public Task DeleteSyncProfileAsync(long id, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Mutations.DeleteSyncProfile", cancellationToken, n: id);
|
||||
public async Task<long> UpsertOperationProfileAsync(OperationProfile profile, CancellationToken cancellationToken = default)
|
||||
=> (await _client.CallAsync("Mutations.UpsertOperationProfile", cancellationToken, payload: Json(profile)).ConfigureAwait(false)).N ?? 0;
|
||||
public Task DeleteOperationProfileAsync(long id, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Mutations.DeleteOperationProfile", cancellationToken, n: id);
|
||||
public async Task<long> CreateRenameBatchAsync(IReadOnlyList<RenameBatchItem> items, CancellationToken cancellationToken = default)
|
||||
=> (await _client.CallAsync("Mutations.CreateRenameBatch", cancellationToken, payload: Json(items)).ConfigureAwait(false)).N ?? 0;
|
||||
public Task MarkRenameBatchUndoneAsync(long id, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Mutations.MarkRenameBatchUndone", cancellationToken, n: id);
|
||||
public Task EnqueueHashCollisionsAsync(long? sourceId, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Mutations.EnqueueHashCollisions", cancellationToken, n: sourceId ?? 0);
|
||||
public Task UpsertRelationAsync(FileRelation relation, CancellationToken cancellationToken = default)
|
||||
=> _client.CallAsync("Mutations.UpsertRelation", cancellationToken, payload: Json(relation));
|
||||
|
||||
private static string Json<T>(T value) => JsonSerializer.Serialize(value, WorkbenchIpc.Json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ public sealed class WorkbenchPipeServer : BackgroundService
|
||||
continue;
|
||||
}
|
||||
|
||||
var response = Handle(request);
|
||||
var response = await HandleAsync(request).ConfigureAwait(false);
|
||||
await WriteAsync(writer, response, stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -139,6 +139,9 @@ public sealed class WorkbenchPipeServer : BackgroundService
|
||||
}
|
||||
|
||||
internal IpcEnvelope Handle(IpcEnvelope request)
|
||||
=> HandleAsync(request).GetAwaiter().GetResult();
|
||||
|
||||
internal async Task<IpcEnvelope> HandleAsync(IpcEnvelope request)
|
||||
{
|
||||
var reply = new IpcEnvelope { Id = request.Id, Ok = true };
|
||||
if (request.V != WorkbenchIpc.ProtocolVersion)
|
||||
@@ -203,6 +206,69 @@ public sealed class WorkbenchPipeServer : BackgroundService
|
||||
case "Transfers.MoveDown":
|
||||
reply.Flag = _workbench.Transfers.MoveDown(request.N ?? 0);
|
||||
return reply;
|
||||
case "Transfers.EnqueueCopy":
|
||||
await _workbench.Transfers.EnqueueCopyAsync(request.Paths ?? [], request.Dest ?? "").ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Transfers.EnqueueMove":
|
||||
await _workbench.Transfers.EnqueueMoveAsync(request.Paths ?? [], request.Dest ?? "").ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Transfers.EnqueueDelete":
|
||||
await _workbench.Transfers.EnqueueDeleteAsync(request.Paths ?? [], request.Flag == true).ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Transfers.EnqueueRename":
|
||||
await _workbench.Transfers.EnqueueRenameAsync(request.S ?? "", request.Dest ?? "").ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Transfers.EnqueueEmptyRecycleBin":
|
||||
await _workbench.Transfers.EnqueueEmptyRecycleBinAsync().ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Transfers.EnqueueExtract":
|
||||
await _workbench.Transfers.EnqueueExtractAsync(request.S ?? "", request.Dest ?? "").ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Transfers.EnqueueCompress":
|
||||
await _workbench.Transfers.EnqueueCompressAsync(request.Paths ?? [], request.Dest ?? "").ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Transfers.EnqueueAddToArchive":
|
||||
await _workbench.Transfers.EnqueueAddToArchiveAsync(request.Dest ?? "", request.Paths ?? []).ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Transfers.EnqueueVerifyArchive":
|
||||
await _workbench.Transfers.EnqueueVerifyArchiveAsync(request.S ?? "").ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Sources.Refresh":
|
||||
await _workbench.Sources.RefreshAsync().ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Sources.AddUnc":
|
||||
reply.Source = await _workbench.Sources.AddUncAsync(request.S ?? "").ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Sources.EnsureForPath":
|
||||
reply.Source = await _workbench.Sources.EnsureForPathAsync(request.S ?? "").ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Sources.Forget":
|
||||
reply.Flag = await _workbench.Sources.ForgetAsync(request.S ?? "").ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Mutations.UpsertSyncProfile":
|
||||
reply.N = await _workbench.Mutations.UpsertSyncProfileAsync(Read<SyncProfile>(request.Payload)).ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Mutations.DeleteSyncProfile":
|
||||
await _workbench.Mutations.DeleteSyncProfileAsync(request.N ?? 0).ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Mutations.UpsertOperationProfile":
|
||||
reply.N = await _workbench.Mutations.UpsertOperationProfileAsync(Read<OperationProfile>(request.Payload)).ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Mutations.DeleteOperationProfile":
|
||||
await _workbench.Mutations.DeleteOperationProfileAsync(request.N ?? 0).ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Mutations.CreateRenameBatch":
|
||||
reply.N = await _workbench.Mutations.CreateRenameBatchAsync(Read<RenameBatchItem[]>(request.Payload) ?? []).ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Mutations.MarkRenameBatchUndone":
|
||||
await _workbench.Mutations.MarkRenameBatchUndoneAsync(request.N ?? 0).ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Mutations.EnqueueHashCollisions":
|
||||
await _workbench.Mutations.EnqueueHashCollisionsAsync(request.N is 0 or null ? null : request.N).ConfigureAwait(false);
|
||||
return reply;
|
||||
case "Mutations.UpsertRelation":
|
||||
await _workbench.Mutations.UpsertRelationAsync(Read<FileRelation>(request.Payload)).ConfigureAwait(false);
|
||||
return reply;
|
||||
default:
|
||||
reply.Ok = false;
|
||||
reply.Error = "Unknown op " + request.Op;
|
||||
@@ -217,6 +283,10 @@ public sealed class WorkbenchPipeServer : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
private static T Read<T>(string? payload)
|
||||
=> JsonSerializer.Deserialize<T>(payload ?? "null", WorkbenchIpc.Json)
|
||||
?? throw new InvalidOperationException("Missing payload for " + typeof(T).Name);
|
||||
|
||||
private async Task WriteAsync(StreamWriter writer, IpcEnvelope envelope, CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(envelope, WorkbenchIpc.Json);
|
||||
|
||||
58
src/Explorer.Hosting/WorkbenchHostConnector.cs
Normal file
58
src/Explorer.Hosting/WorkbenchHostConnector.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System.Diagnostics;
|
||||
using Explorer.Hosting.Ipc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Explorer.Hosting;
|
||||
|
||||
public static class WorkbenchHostConnector
|
||||
{
|
||||
public static async Task<WorkbenchPipeClient?> ConnectOrStartAsync(
|
||||
TimeSpan timeout,
|
||||
ILogger? logger = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var options = new WorkbenchIpcOptions();
|
||||
try
|
||||
{
|
||||
return await WorkbenchPipeClient.ConnectAsync(options, TimeSpan.FromSeconds(1), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
logger?.LogDebug(ex, "Background host was not listening yet");
|
||||
}
|
||||
|
||||
var exe = HostLogonAutostart.FindHostExecutable();
|
||||
if (exe is null)
|
||||
{
|
||||
logger?.LogInformation("Explorer.Host.exe is not beside the window; using in-process core");
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = exe,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = Path.GetDirectoryName(exe)
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "Could not start Explorer.Host.exe");
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await WorkbenchPipeClient.ConnectAsync(options, timeout, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
logger?.LogWarning(ex, "Could not connect to Explorer.Host.exe");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,14 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Explorer.Analysis;
|
||||
using Explorer.Application;
|
||||
using Explorer.Contracts;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
|
||||
namespace Explorer.Presentation.ViewModels;
|
||||
|
||||
public sealed partial class DuplicateViewModel : ObservableObject
|
||||
{
|
||||
private readonly IIndexStore _store;
|
||||
private readonly IIndexMutations _mutations;
|
||||
private readonly SourceManager _sources;
|
||||
private readonly AnalysisService _analysis;
|
||||
|
||||
@@ -20,9 +20,9 @@ public sealed partial class DuplicateViewModel : ObservableObject
|
||||
[ObservableProperty] private bool _showIntentional;
|
||||
[ObservableProperty] private bool _showHardlinks;
|
||||
|
||||
public DuplicateViewModel(IIndexStore store, SourceManager sources, AnalysisService analysis)
|
||||
public DuplicateViewModel(IIndexMutations mutations, SourceManager sources, AnalysisService analysis)
|
||||
{
|
||||
_store = store;
|
||||
_mutations = mutations;
|
||||
_sources = sources;
|
||||
_analysis = analysis;
|
||||
Groups = [];
|
||||
@@ -50,7 +50,7 @@ public sealed partial class DuplicateViewModel : ObservableObject
|
||||
{
|
||||
var groups = await Task.Run(async () =>
|
||||
{
|
||||
await _store.Hashes.EnqueueSizeCollisionsAsync(null).ConfigureAwait(false);
|
||||
await _mutations.EnqueueHashCollisionsAsync(null).ConfigureAwait(false);
|
||||
var classified = await _analysis.GetClassifiedDuplicatesAsync(200).ConfigureAwait(false);
|
||||
var sources = (await _sources.RefreshOnlineStateAsync().ConfigureAwait(false)).ToDictionary(s => s.Id);
|
||||
return classified
|
||||
|
||||
@@ -66,7 +66,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
SourceManager sources,
|
||||
SearchService search,
|
||||
AnalysisService analysis,
|
||||
IIndexStore store,
|
||||
IIndexMutations mutations,
|
||||
IWorkbenchHost workbench,
|
||||
IOsClipboard clipboard,
|
||||
PathHistoryStore pathHistory,
|
||||
@@ -109,7 +109,7 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
Tree = new NavigationTreeViewModel(sources, browse, providers, cloudPlaces, preferences);
|
||||
Search = new SearchViewModel(search, sources, volumes);
|
||||
Analysis = new AnalysisViewModel(analysis);
|
||||
Duplicates = new DuplicateViewModel(store, sources, analysis);
|
||||
Duplicates = new DuplicateViewModel(mutations, sources, analysis);
|
||||
Duplicates.RevealPath += (_, path) => _ = RevealDuplicateAsync(path);
|
||||
Transfers = new TransferQueueViewModel(workbench.Transfers, preferences);
|
||||
Tabs = [];
|
||||
|
||||
@@ -10,6 +10,7 @@ public sealed class SqliteIndexStore : IIndexStore, IAsyncDisposable
|
||||
{
|
||||
private readonly string _path;
|
||||
private readonly ILogger<SqliteIndexStore> _logger;
|
||||
private readonly bool _readOnly;
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
private readonly AsyncLocal<int> _writeDepth = new();
|
||||
private SqliteConnection? _write;
|
||||
@@ -17,10 +18,11 @@ public sealed class SqliteIndexStore : IIndexStore, IAsyncDisposable
|
||||
private bool _opened;
|
||||
private int _analysisIndexesReady;
|
||||
|
||||
public SqliteIndexStore(string databasePath, ILogger<SqliteIndexStore> logger)
|
||||
public SqliteIndexStore(string databasePath, ILogger<SqliteIndexStore> logger, bool readOnly = false)
|
||||
{
|
||||
_path = databasePath;
|
||||
_logger = logger;
|
||||
_readOnly = readOnly;
|
||||
Sources = new SourceStore(this);
|
||||
Entries = new EntryStore(this);
|
||||
Excludes = new ExcludeStore(this);
|
||||
@@ -50,6 +52,8 @@ public sealed class SqliteIndexStore : IIndexStore, IAsyncDisposable
|
||||
public ISyncProfileStore SyncProfiles { get; }
|
||||
public IOperationProfileStore OperationProfiles { get; }
|
||||
|
||||
public bool CanWrite => !_readOnly;
|
||||
|
||||
internal SqliteConnection Write => _write ?? throw new InvalidOperationException("Store is not open.");
|
||||
|
||||
public async Task OpenAsync(CancellationToken cancellationToken = default)
|
||||
@@ -59,6 +63,12 @@ public sealed class SqliteIndexStore : IIndexStore, IAsyncDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (_readOnly)
|
||||
{
|
||||
await OpenReadOnlyAsync(cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_lock = IndexStoreLock.Acquire(_path);
|
||||
try
|
||||
{
|
||||
@@ -84,6 +94,48 @@ public sealed class SqliteIndexStore : IIndexStore, IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenReadOnlyAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
DapperSetup.Ensure();
|
||||
Exception? last = null;
|
||||
for (var attempt = 0; attempt < 40; attempt++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_path))
|
||||
{
|
||||
throw new InvalidOperationException("Index database is not ready yet.");
|
||||
}
|
||||
|
||||
_write = new SqliteConnection(BuildConnectionString(_path, readOnly: true));
|
||||
await _write.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
ApplyReadPragmas(_write);
|
||||
if (IndexExists(_write, "ix_entries_dir_agg_all"))
|
||||
{
|
||||
Volatile.Write(ref _analysisIndexesReady, 1);
|
||||
}
|
||||
|
||||
_opened = true;
|
||||
_logger.LogInformation("Opened index database read-only at {Path}", _path);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
last = ex;
|
||||
if (_write is not null)
|
||||
{
|
||||
await _write.DisposeAsync().ConfigureAwait(false);
|
||||
_write = null;
|
||||
}
|
||||
|
||||
await Task.Delay(250, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Could not open the index for read. Is Explorer.Host.exe running?", last);
|
||||
}
|
||||
|
||||
public async Task CloseAsync()
|
||||
{
|
||||
try
|
||||
@@ -114,6 +166,11 @@ public sealed class SqliteIndexStore : IIndexStore, IAsyncDisposable
|
||||
|
||||
public async Task RunWriteAsync(Func<IIndexStore, Task> work, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_readOnly)
|
||||
{
|
||||
throw new InvalidOperationException("Index store is open read-only.");
|
||||
}
|
||||
|
||||
await EnterWriteAsync(cancellationToken).ConfigureAwait(false);
|
||||
var outermost = _writeDepth.Value == 1;
|
||||
SqliteTransaction? tx = null;
|
||||
@@ -165,6 +222,11 @@ public sealed class SqliteIndexStore : IIndexStore, IAsyncDisposable
|
||||
throw new InvalidOperationException("Store is not open.");
|
||||
}
|
||||
|
||||
if (_readOnly)
|
||||
{
|
||||
throw new InvalidOperationException("Index store is open read-only.");
|
||||
}
|
||||
|
||||
await EnterWriteAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
@@ -211,17 +273,17 @@ public sealed class SqliteIndexStore : IIndexStore, IAsyncDisposable
|
||||
|
||||
internal async Task<SqliteConnection> OpenReadAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var conn = new SqliteConnection(BuildConnectionString(_path));
|
||||
var conn = new SqliteConnection(BuildConnectionString(_path, _readOnly));
|
||||
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
ApplyReadPragmas(conn);
|
||||
return conn;
|
||||
}
|
||||
|
||||
internal static string BuildConnectionString(string path)
|
||||
internal static string BuildConnectionString(string path, bool readOnly = false)
|
||||
=> new SqliteConnectionStringBuilder
|
||||
{
|
||||
DataSource = path,
|
||||
Mode = SqliteOpenMode.ReadWriteCreate,
|
||||
Mode = readOnly ? SqliteOpenMode.ReadOnly : SqliteOpenMode.ReadWriteCreate,
|
||||
Pooling = false
|
||||
}.ToString();
|
||||
|
||||
@@ -249,6 +311,11 @@ public sealed class SqliteIndexStore : IIndexStore, IAsyncDisposable
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (_readOnly)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return WriteAsync(conn =>
|
||||
{
|
||||
EnsureAnalysisIndexes(conn);
|
||||
|
||||
Reference in New Issue
Block a user