diff --git a/src/Server/FileTime.Server.App.Abstractions/FileTime.Server.App.Abstractions.csproj b/src/Server/FileTime.Server.App.Abstractions/FileTime.Server.App.Abstractions.csproj
new file mode 100644
index 0000000..a1ed5b3
--- /dev/null
+++ b/src/Server/FileTime.Server.App.Abstractions/FileTime.Server.App.Abstractions.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
diff --git a/src/Server/FileTime.Server.App/DummyAdminElevationManager.cs b/src/Server/FileTime.Server.App/DummyAdminElevationManager.cs
new file mode 100644
index 0000000..c7c2ddf
--- /dev/null
+++ b/src/Server/FileTime.Server.App/DummyAdminElevationManager.cs
@@ -0,0 +1,14 @@
+using FileTime.Providers.LocalAdmin;
+using FileTime.Server.Common;
+
+namespace FileTime.Server.App;
+
+public class DummyAdminElevationManager : IAdminElevationManager
+{
+ public bool IsAdminInstanceRunning => throw new NotImplementedException();
+ public Task CreateConnectionAsync() => throw new NotImplementedException();
+
+ public string ProviderName => throw new NotImplementedException();
+ public Task CreateAdminInstanceIfNecessaryAsync(string? confirmationMessage = null) => throw new NotImplementedException();
+ public bool IsAdminModeSupported => false;
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server.App/FileTime.Server.App.csproj b/src/Server/FileTime.Server.App/FileTime.Server.App.csproj
new file mode 100644
index 0000000..b4c49ac
--- /dev/null
+++ b/src/Server/FileTime.Server.App/FileTime.Server.App.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Server/FileTime.Server.App/Startup.cs b/src/Server/FileTime.Server.App/Startup.cs
new file mode 100644
index 0000000..a240091
--- /dev/null
+++ b/src/Server/FileTime.Server.App/Startup.cs
@@ -0,0 +1,13 @@
+using FileTime.Providers.LocalAdmin;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace FileTime.Server.App;
+
+public static class Startup
+{
+ public static IServiceCollection AddServerServices(this IServiceCollection serviceCollection)
+ {
+ serviceCollection.AddSingleton();
+ return serviceCollection;
+ }
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server.Common.Abstractions/FileTime.Server.Common.Abstractions.csproj b/src/Server/FileTime.Server.Common.Abstractions/FileTime.Server.Common.Abstractions.csproj
new file mode 100644
index 0000000..aa2bde0
--- /dev/null
+++ b/src/Server/FileTime.Server.Common.Abstractions/FileTime.Server.Common.Abstractions.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net7.0
+ enable
+ enable
+ FileTime.Server.Common
+
+
+
+
+
+
+
diff --git a/src/Server/FileTime.Server.Common.Abstractions/IApplicationStopper.cs b/src/Server/FileTime.Server.Common.Abstractions/IApplicationStopper.cs
new file mode 100644
index 0000000..7a8fcb2
--- /dev/null
+++ b/src/Server/FileTime.Server.Common.Abstractions/IApplicationStopper.cs
@@ -0,0 +1,6 @@
+namespace FileTime.Server.Common;
+
+public interface IApplicationStopper
+{
+ void Stop();
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server.Common.Abstractions/IRemoteConnection.cs b/src/Server/FileTime.Server.Common.Abstractions/IRemoteConnection.cs
new file mode 100644
index 0000000..ee3e79e
--- /dev/null
+++ b/src/Server/FileTime.Server.Common.Abstractions/IRemoteConnection.cs
@@ -0,0 +1,10 @@
+using FileTime.Core.Models;
+
+namespace FileTime.Server.Common;
+
+public interface IRemoteConnection
+{
+ Task Exit();
+ Task CreateContainerAsync(string contentProviderId, FullName fullName);
+ Task CreateElementAsync(string contentProviderId, FullName fullName);
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server.Common/ApplicationStopper.cs b/src/Server/FileTime.Server.Common/ApplicationStopper.cs
new file mode 100644
index 0000000..7a55bcc
--- /dev/null
+++ b/src/Server/FileTime.Server.Common/ApplicationStopper.cs
@@ -0,0 +1,15 @@
+namespace FileTime.Server.Common;
+
+public class ApplicationStopper : IApplicationStopper
+{
+ private readonly Action _stopAction;
+
+ public ApplicationStopper(Action stopAction)
+ {
+ ArgumentNullException.ThrowIfNull(stopAction);
+ _stopAction = stopAction;
+ }
+
+
+ public void Stop() => _stopAction();
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server.Common/Connections/SignalR/ISignalRClient.cs b/src/Server/FileTime.Server.Common/Connections/SignalR/ISignalRClient.cs
new file mode 100644
index 0000000..bd55598
--- /dev/null
+++ b/src/Server/FileTime.Server.Common/Connections/SignalR/ISignalRClient.cs
@@ -0,0 +1,6 @@
+namespace FileTime.Server.Common.Connections.SignalR;
+
+public interface ISignalRClient
+{
+
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server.Common/Connections/SignalR/ISignalRHub.cs b/src/Server/FileTime.Server.Common/Connections/SignalR/ISignalRHub.cs
new file mode 100644
index 0000000..7487070
--- /dev/null
+++ b/src/Server/FileTime.Server.Common/Connections/SignalR/ISignalRHub.cs
@@ -0,0 +1,8 @@
+namespace FileTime.Server.Common.Connections.SignalR;
+
+public interface ISignalRHub
+{
+ Task Exit();
+ Task CreateContainerAsync(string contentProviderId, string fullName);
+ Task CreateElementAsync(string contentProviderId, string fullName);
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server.Common/Connections/SignalR/SignalRConnection.cs b/src/Server/FileTime.Server.Common/Connections/SignalR/SignalRConnection.cs
new file mode 100644
index 0000000..0993da5
--- /dev/null
+++ b/src/Server/FileTime.Server.Common/Connections/SignalR/SignalRConnection.cs
@@ -0,0 +1,42 @@
+using FileTime.Core.Models;
+using InitableService;
+using Microsoft.AspNetCore.SignalR.Client;
+using TypedSignalR.Client;
+
+namespace FileTime.Server.Common.Connections.SignalR;
+
+public class SignalRConnection : IRemoteConnection, IAsyncInitable
+{
+ private string _baseUrl = null!;
+ private HubConnection _connection = null!;
+
+ private ISignalRHub CreateClient() => _connection.CreateHubProxy();
+
+ public async Task InitAsync(string baseUrl)
+ {
+ _baseUrl = baseUrl;
+
+ _connection = new HubConnectionBuilder()
+ .WithUrl(_baseUrl)
+ .Build();
+ await _connection.StartAsync();
+ }
+
+ public async Task Exit()
+ {
+ var client = CreateClient();
+ await client.Exit();
+ }
+
+ public async Task CreateContainerAsync(string contentProviderId, FullName fullName)
+ {
+ var client = CreateClient();
+ await client.CreateContainerAsync(contentProviderId, fullName.Path);
+ }
+
+ public async Task CreateElementAsync(string contentProviderId, FullName fullName)
+ {
+ var client = CreateClient();
+ await client.CreateElementAsync(contentProviderId, fullName.Path);
+ }
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server.Common/FileTime.Server.Common.csproj b/src/Server/FileTime.Server.Common/FileTime.Server.Common.csproj
new file mode 100644
index 0000000..5908eec
--- /dev/null
+++ b/src/Server/FileTime.Server.Common/FileTime.Server.Common.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/src/Server/FileTime.Server.Common/Startup.cs b/src/Server/FileTime.Server.Common/Startup.cs
new file mode 100644
index 0000000..4c34e58
--- /dev/null
+++ b/src/Server/FileTime.Server.Common/Startup.cs
@@ -0,0 +1,14 @@
+using FileTime.Server.Common.Connections.SignalR;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace FileTime.Server.Common;
+
+public static class Startup
+{
+ public static IServiceCollection AddRemoteServices(this IServiceCollection services)
+ {
+ services.AddTransient();
+ services.AddSingleton();
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server.Web/ConnectionHub.cs b/src/Server/FileTime.Server.Web/ConnectionHub.cs
new file mode 100644
index 0000000..99b8032
--- /dev/null
+++ b/src/Server/FileTime.Server.Web/ConnectionHub.cs
@@ -0,0 +1,45 @@
+using FileTime.Core.ContentAccess;
+using FileTime.Core.Models;
+using FileTime.Server.Common;
+using FileTime.Server.Common.Connections.SignalR;
+using Microsoft.AspNetCore.SignalR;
+
+namespace FileTime.Server.Web;
+
+public class ConnectionHub : Hub, ISignalRHub
+{
+ private readonly IContentProviderRegistry _contentProviderRegistry;
+ private readonly IContentAccessorFactory _contentAccessorFactory;
+ private readonly IApplicationStopper _applicationStopper;
+
+ public ConnectionHub(
+ IContentProviderRegistry contentProviderRegistry,
+ IContentAccessorFactory contentAccessorFactory,
+ IApplicationStopper applicationStopper)
+ {
+ _contentProviderRegistry = contentProviderRegistry;
+ _contentAccessorFactory = contentAccessorFactory;
+ _applicationStopper = applicationStopper;
+ }
+
+ public Task Exit()
+ {
+ _applicationStopper.Stop();
+ return Task.CompletedTask;
+ }
+
+ public async Task CreateContainerAsync(string contentProviderId, string fullName)
+ {
+ //TODO handle no content provider with id
+ var contentProvider = _contentProviderRegistry.ContentProviders.First(p => p.Name == contentProviderId);
+ var itemCreator = _contentAccessorFactory.GetItemCreator(contentProvider);
+ await itemCreator.CreateContainerAsync(contentProvider, new FullName(fullName));
+ }
+
+ public async Task CreateElementAsync(string contentProviderId, string fullName)
+ {
+ var contentProvider = _contentProviderRegistry.ContentProviders.First(p => p.Name == contentProviderId);
+ var itemCreator = _contentAccessorFactory.GetItemCreator(contentProvider);
+ await itemCreator.CreateElementAsync(contentProvider, new FullName(fullName));
+ }
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server.Web/FileTime.Server.Web.csproj b/src/Server/FileTime.Server.Web/FileTime.Server.Web.csproj
new file mode 100644
index 0000000..01f9af4
--- /dev/null
+++ b/src/Server/FileTime.Server.Web/FileTime.Server.Web.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Server/FileTime.Server.Web/Main.cs b/src/Server/FileTime.Server.Web/Main.cs
new file mode 100644
index 0000000..3c62f9c
--- /dev/null
+++ b/src/Server/FileTime.Server.Web/Main.cs
@@ -0,0 +1,53 @@
+using System.Net;
+using Autofac;
+using Autofac.Extensions.DependencyInjection;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Serilog;
+
+namespace FileTime.Server.Web;
+
+public class Program
+{
+ public static async Task Start(string[] args, IContainer rootContainer, CancellationToken applicationExit)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ var configuration = builder.Configuration;
+
+ builder.Host.UseServiceProviderFactory(
+ new AutofacChildLifetimeScopeServiceProviderFactory(rootContainer.BeginLifetimeScope("WebScope"))
+ );
+
+ builder.Host.UseSerilog();
+ builder.WebHost.ConfigureKestrel((buildContext, serverOptions) =>
+ {
+ var port = buildContext.Configuration.GetValue("WebPort") ?? 0;
+ serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, port));
+ });
+
+ builder.Services.AddSignalR();
+ builder.Services.AddHealthChecks();
+ builder.Services.AddHostedService();
+
+ builder.Services.AddOptions()
+ .Bind(configuration.GetSection(PortWriterConfiguration.SectionName));
+
+ var app = builder.Build();
+
+ if (!app.Environment.IsDevelopment())
+ {
+ }
+
+ app.MapHub("/RemoteHub");
+ app.UseHealthChecks("/health");
+
+ await app.RunAsync(applicationExit);
+ }
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server.Web/PortWriterConfiguration.cs b/src/Server/FileTime.Server.Web/PortWriterConfiguration.cs
new file mode 100644
index 0000000..277871c
--- /dev/null
+++ b/src/Server/FileTime.Server.Web/PortWriterConfiguration.cs
@@ -0,0 +1,7 @@
+namespace FileTime.Server.Web;
+
+public class PortWriterConfiguration
+{
+ public const string SectionName = "PortWriter";
+ public string? Filename { get; set; }
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server.Web/PortWriterService.cs b/src/Server/FileTime.Server.Web/PortWriterService.cs
new file mode 100644
index 0000000..609dd9e
--- /dev/null
+++ b/src/Server/FileTime.Server.Web/PortWriterService.cs
@@ -0,0 +1,79 @@
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace FileTime.Server.Web;
+
+public class PortWriterService : IHostedService
+{
+ private readonly IServer _server;
+ private readonly IHostApplicationLifetime _hostApplicationLifetime;
+ private readonly IOptions _configuration;
+ private readonly ILogger _logger;
+
+ public PortWriterService(
+ IServer server,
+ IHostApplicationLifetime hostApplicationLifetime,
+ IOptions configuration,
+ ILogger logger)
+ {
+ _server = server;
+ _hostApplicationLifetime = hostApplicationLifetime;
+ _configuration = configuration;
+ _logger = logger;
+ }
+
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _hostApplicationLifetime.ApplicationStarted.Register(WritePort);
+
+ return Task.CompletedTask;
+ }
+
+ private void WritePort()
+ {
+ try
+ {
+ var filename = _configuration.Value.Filename;
+ if (filename is null)
+ {
+ _logger.LogWarning("Could not save port to file as there were no file name given");
+ return;
+ }
+
+ using var tempFileStream = File.CreateText(filename);
+ var address = GetAddress();
+ if (address is null)
+ {
+ _logger.LogError("Could not get address");
+ return;
+ }
+
+ var couldParsePort = int.TryParse(address.Split(':').LastOrDefault(), out var port);
+ if (!couldParsePort)
+ {
+ _logger.LogError("Could not parse port from address {Address}", address);
+ return;
+ }
+
+ tempFileStream.Write(port.ToString());
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not save port to file");
+ }
+ }
+
+ private string? GetAddress()
+ {
+ var features = _server.Features;
+ var addresses = features.Get();
+ var address = addresses?.Addresses.FirstOrDefault();
+ return address;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}
\ No newline at end of file
diff --git a/src/Server/FileTime.Server/FileTime.Server.csproj b/src/Server/FileTime.Server/FileTime.Server.csproj
new file mode 100644
index 0000000..d87b452
--- /dev/null
+++ b/src/Server/FileTime.Server/FileTime.Server.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Server/FileTime.Server/Program.cs b/src/Server/FileTime.Server/Program.cs
new file mode 100644
index 0000000..f64e150
--- /dev/null
+++ b/src/Server/FileTime.Server/Program.cs
@@ -0,0 +1,64 @@
+using Autofac;
+using Autofac.Extensions.DependencyInjection;
+using FileTime.App.DependencyInjection;
+using FileTime.Providers.Local;
+using FileTime.Server.App;
+using FileTime.Server.Common;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Serilog;
+
+
+var applicationCancellation = new CancellationTokenSource();
+Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Debug()
+ .WriteTo.Console()
+ .CreateLogger();
+
+var bootstrapConfiguration = CreateConfiguration();
+
+var rootContainer = CreateRootDiContainer(bootstrapConfiguration);
+
+var webThread = CreateStartup(FileTime.Server.Web.Program.Start);
+webThread.Start();
+
+Thread CreateStartup(Func startup)
+{
+ var thread = new Thread(() => { HandleStartup(() => startup(args, rootContainer, applicationCancellation.Token).Wait()); });
+ return thread;
+}
+
+void HandleStartup(Action action)
+{
+ try
+ {
+ action();
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ throw;
+ }
+}
+
+IConfigurationRoot CreateConfiguration()
+{
+ var configurationBuilder = new ConfigurationBuilder();
+ return configurationBuilder.Build();
+}
+
+IContainer CreateRootDiContainer(IConfigurationRoot configuration)
+{
+ var serviceCollection = DependencyInjection
+ .RegisterDefaultServices(configuration)
+ .AddLocalProviderServices()
+ .AddServerServices();
+
+ serviceCollection.TryAddSingleton(
+ new ApplicationStopper(() => applicationCancellation.Cancel())
+ );
+
+ var containerBuilder = new ContainerBuilder();
+ containerBuilder.Populate(serviceCollection);
+ return containerBuilder.Build();
+}
\ No newline at end of file