App
This commit is contained in:
10
src/Alma.Abstraction/Alma.Abstraction.csproj
Normal file
10
src/Alma.Abstraction/Alma.Abstraction.csproj
Normal file
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Alma</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
7
src/Alma.Abstraction/Command/ICommand.cs
Normal file
7
src/Alma.Abstraction/Command/ICommand.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Alma.Command;
|
||||
|
||||
public interface ICommand
|
||||
{
|
||||
public string CommandString { get; }
|
||||
public Task Run(List<string> parameters);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Alma.Configuration.Module;
|
||||
|
||||
public record ModuleConfiguration(string? Target, Dictionary<string, string>? Links)
|
||||
{
|
||||
public ModuleConfiguration Merge(ModuleConfiguration merge)
|
||||
{
|
||||
var mergedLinks = (Links ?? new Dictionary<string, string>())
|
||||
.Concat(merge.Links ?? new Dictionary<string, string>());
|
||||
return new ModuleConfiguration(
|
||||
merge.Target ?? Target,
|
||||
new Dictionary<string, string>(mergedLinks)
|
||||
);
|
||||
}
|
||||
|
||||
public static ModuleConfiguration Empty() =>
|
||||
new(null, new Dictionary<string, string>());
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Alma.Configuration.Module;
|
||||
|
||||
public class ModuleConfigurationRoot : Dictionary<string, ModuleConfiguration>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Alma.Configuration.Repository;
|
||||
|
||||
public interface IRepositoryConfiguration
|
||||
{
|
||||
public Task LoadAsync();
|
||||
RepositoryConfigurationRoot Configuration { get; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Alma.Configuration.Repository;
|
||||
|
||||
public record RepositoryConfigurationEntry(
|
||||
string Name,
|
||||
string? RepositoryPath,
|
||||
string? LinkPath
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Alma.Configuration.Repository;
|
||||
|
||||
public record RepositoryConfigurationRoot(List<RepositoryConfigurationEntry> Repositories);
|
||||
6
src/Alma.Abstraction/Data/Constants.cs
Normal file
6
src/Alma.Abstraction/Data/Constants.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Alma.Data;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public static readonly string ModuleConfigFileStub = ".alma-config";
|
||||
}
|
||||
3
src/Alma.Abstraction/Data/ItemToLink.cs
Normal file
3
src/Alma.Abstraction/Data/ItemToLink.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Alma.Data;
|
||||
|
||||
public record ItemToLink(string SourcePath, string TargetPath);
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Alma.Services;
|
||||
|
||||
public interface IConfigurationFileReader
|
||||
{
|
||||
public Task<(T? Result, string? FileName)> DeserializeAsync<T>(string fileNameWithoutExtension, string? extension = null) where T : class;
|
||||
}
|
||||
7
src/Alma.Abstraction/Services/IFolderService.cs
Normal file
7
src/Alma.Abstraction/Services/IFolderService.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Alma.Services;
|
||||
|
||||
public interface IFolderService
|
||||
{
|
||||
string? ConfigRoot { get; }
|
||||
string AppData { get; }
|
||||
}
|
||||
8
src/Alma.Abstraction/Services/IMetadataHandler.cs
Normal file
8
src/Alma.Abstraction/Services/IMetadataHandler.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Alma.Data;
|
||||
|
||||
namespace Alma.Services;
|
||||
|
||||
public interface IMetadataHandler
|
||||
{
|
||||
Task SaveLinkedItemsAsync(List<ItemToLink> successfulLinks, DirectoryInfo sourceDirectory, DirectoryInfo targetDirectory);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Alma.Configuration.Module;
|
||||
|
||||
namespace Alma.Services;
|
||||
|
||||
public interface IModuleConfigurationResolver
|
||||
{
|
||||
Task<(ModuleConfiguration? mergedModuleConfig, string? moduleConfigFileName)> ResolveModuleConfiguration(string moduleConfigStub);
|
||||
}
|
||||
7
src/Alma.Abstraction/Services/IOsInformation.cs
Normal file
7
src/Alma.Abstraction/Services/IOsInformation.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Alma.Services;
|
||||
|
||||
public interface IOsInformation
|
||||
{
|
||||
string GetOsIdentifier();
|
||||
bool IsOnPlatform(string platform);
|
||||
}
|
||||
14
src/Alma.App/Alma.App.csproj
Normal file
14
src/Alma.App/Alma.App.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Alma.Abstraction\Alma.Abstraction.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Alma</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
37
src/Alma.App/Application.cs
Normal file
37
src/Alma.App/Application.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Alma.Command;
|
||||
using Alma.Command.Help;
|
||||
|
||||
namespace Alma;
|
||||
|
||||
public class Application
|
||||
{
|
||||
private readonly IList<ICommand> _commands;
|
||||
|
||||
public Application(IEnumerable<ICommand> commands)
|
||||
{
|
||||
_commands = commands.Append(new HelpCommand(() => _commands!)).ToList();
|
||||
}
|
||||
|
||||
public async Task Run(string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Console.WriteLine("No command was given");
|
||||
return;
|
||||
}
|
||||
|
||||
var commandString = args[0];
|
||||
|
||||
var command = _commands.FirstOrDefault(c => c.CommandString == commandString);
|
||||
|
||||
if (command is null)
|
||||
{
|
||||
Console.WriteLine($"Invalid command: {commandString}");
|
||||
return;
|
||||
}
|
||||
|
||||
await command.Run(args[1..].ToList());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
24
src/Alma.App/Command/Help/HelpCommand.cs
Normal file
24
src/Alma.App/Command/Help/HelpCommand.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace Alma.Command.Help;
|
||||
|
||||
public class HelpCommand : ICommand
|
||||
{
|
||||
private readonly Func<IEnumerable<ICommand>> _commandsProvider;
|
||||
public string CommandString => "help";
|
||||
|
||||
public HelpCommand(Func<IEnumerable<ICommand>> commandsProvider)
|
||||
{
|
||||
_commandsProvider = commandsProvider;
|
||||
}
|
||||
|
||||
public Task Run(List<string> parameters)
|
||||
{
|
||||
Console.WriteLine("Commands:" + Environment.NewLine);
|
||||
|
||||
foreach (var command in _commandsProvider().OrderBy(c => c.CommandString))
|
||||
{
|
||||
Console.WriteLine(command.CommandString);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
10
src/Alma.App/Command/Info/ModuleInfoCommand.cs
Normal file
10
src/Alma.App/Command/Info/ModuleInfoCommand.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Alma.Command.Info;
|
||||
|
||||
public class ModuleInfoCommand : ICommand
|
||||
{
|
||||
public string CommandString => "info";
|
||||
public Task Run(List<string> parameters)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
193
src/Alma.App/Command/Link/LinkCommand.cs
Normal file
193
src/Alma.App/Command/Link/LinkCommand.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using Alma.Configuration.Module;
|
||||
using Alma.Configuration.Repository;
|
||||
using Alma.Data;
|
||||
using Alma.Services;
|
||||
|
||||
namespace Alma.Command.Link;
|
||||
|
||||
public class LinkCommand : ICommand
|
||||
{
|
||||
private readonly IRepositoryConfiguration _repositoryConfiguration;
|
||||
private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IMetadataHandler _metadataHandler;
|
||||
public string CommandString => "link";
|
||||
|
||||
public LinkCommand(
|
||||
IRepositoryConfiguration repositoryConfiguration,
|
||||
IModuleConfigurationResolver moduleConfigurationResolver,
|
||||
IFolderService folderService,
|
||||
IMetadataHandler metadataHandler)
|
||||
{
|
||||
_repositoryConfiguration = repositoryConfiguration;
|
||||
_moduleConfigurationResolver = moduleConfigurationResolver;
|
||||
_folderService = folderService;
|
||||
_metadataHandler = metadataHandler;
|
||||
}
|
||||
|
||||
public async Task Run(List<string> parameters)
|
||||
{
|
||||
if (parameters.Count == 0)
|
||||
{
|
||||
Console.WriteLine("No module specified");
|
||||
return;
|
||||
}
|
||||
|
||||
string moduleName = parameters[0];
|
||||
|
||||
string sourceDirectory = Path.Combine(Environment.CurrentDirectory);
|
||||
string targetDirectory = Path.Combine(Environment.CurrentDirectory, "..");
|
||||
|
||||
var repoName = GetRepositoryName(parameters);
|
||||
if (repoName is not null
|
||||
&& _repositoryConfiguration.Configuration.Repositories.FirstOrDefault(r => r.Name == repoName) is { } repoConfig)
|
||||
{
|
||||
sourceDirectory = repoConfig.RepositoryPath ?? sourceDirectory;
|
||||
targetDirectory = repoConfig.LinkPath ?? targetDirectory;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(sourceDirectory))
|
||||
{
|
||||
Console.WriteLine("Source directory not exists: " + sourceDirectory);
|
||||
return;
|
||||
}
|
||||
|
||||
string moduleNameAsPath = moduleName.Replace('/', Path.DirectorySeparatorChar);
|
||||
string moduleDirectory = Path.Combine(sourceDirectory, moduleNameAsPath);
|
||||
|
||||
if (!Directory.Exists(moduleDirectory))
|
||||
{
|
||||
Console.WriteLine("Module directory not exists: " + moduleDirectory);
|
||||
return;
|
||||
}
|
||||
|
||||
var moduleConfigFileStub = Path.Combine(moduleDirectory, Constants.ModuleConfigFileStub);
|
||||
var (moduleConfiguration, moduleConfigurationFile) = await _moduleConfigurationResolver.ResolveModuleConfiguration(moduleConfigFileStub);
|
||||
|
||||
if (moduleConfiguration?.Target is string moduleTargetDir)
|
||||
{
|
||||
targetDirectory = ResolvePath(moduleTargetDir, targetDirectory);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(targetDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
}
|
||||
|
||||
var moduleConfigurationFileFullPath = moduleConfigurationFile is null ? null : Path.Combine(moduleDirectory, moduleConfigurationFile);
|
||||
|
||||
var moduleDir = new DirectoryInfo(moduleDirectory);
|
||||
var currentTargetDirectory = new DirectoryInfo(targetDirectory);
|
||||
var itemsToLink = (await TraverseTree(
|
||||
moduleDir,
|
||||
currentTargetDirectory,
|
||||
moduleDir,
|
||||
currentTargetDirectory,
|
||||
moduleConfiguration)).ToList();
|
||||
if (moduleConfigurationFile is not null) itemsToLink.RemoveAll(i => i.SourcePath == moduleConfigurationFileFullPath);
|
||||
|
||||
foreach (var itemToLink in itemsToLink)
|
||||
{
|
||||
Console.WriteLine($"Link: '{itemToLink.SourcePath}' '{itemToLink.TargetPath}'");
|
||||
}
|
||||
|
||||
var successfulLinks = CreateLinks(itemsToLink);
|
||||
|
||||
await _metadataHandler.SaveLinkedItemsAsync(successfulLinks, moduleDir, currentTargetDirectory);
|
||||
//await _metadataHandler.SaveLinkedItemsAsync(itemsToLink, moduleDir, currentTargetDirectory);
|
||||
}
|
||||
|
||||
private List<ItemToLink> CreateLinks(List<ItemToLink> itemsToLink)
|
||||
{
|
||||
var successfulLinks = new List<ItemToLink>();
|
||||
|
||||
foreach (var itemToLink in itemsToLink)
|
||||
{
|
||||
if (File.Exists(itemToLink.TargetPath) || Directory.Exists(itemToLink.TargetPath))
|
||||
{
|
||||
Console.WriteLine("Item already exists: " + itemToLink.TargetPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourceFileExists = File.Exists(itemToLink.SourcePath);
|
||||
var sourceDirectoryExists = Directory.Exists(itemToLink.SourcePath);
|
||||
|
||||
if (sourceFileExists)
|
||||
{
|
||||
File.CreateSymbolicLink(itemToLink.TargetPath, itemToLink.SourcePath);
|
||||
}
|
||||
else if (sourceDirectoryExists)
|
||||
{
|
||||
File.CreateSymbolicLink(itemToLink.TargetPath, itemToLink.SourcePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Source not exists: " + itemToLink.SourcePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
successfulLinks.Add(itemToLink);
|
||||
}
|
||||
|
||||
return successfulLinks;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<ItemToLink>> TraverseTree(
|
||||
DirectoryInfo currentDirectory,
|
||||
DirectoryInfo currentTargetDirectory,
|
||||
DirectoryInfo moduleDirectory,
|
||||
DirectoryInfo targetDirectory,
|
||||
ModuleConfiguration? moduleConfiguration)
|
||||
{
|
||||
var filesToLink = new List<ItemToLink>();
|
||||
foreach (var file in currentDirectory.GetFiles())
|
||||
{
|
||||
filesToLink.Add(new ItemToLink(Path.Combine(currentDirectory.FullName, file.Name), Path.Combine(currentTargetDirectory.FullName, file.Name)));
|
||||
}
|
||||
|
||||
var subDirLinksToAdd = Enumerable.Empty<ItemToLink>();
|
||||
|
||||
foreach (var subDir in currentDirectory.GetDirectories())
|
||||
{
|
||||
var relativePath = GetRelativePath(subDir.FullName, moduleDirectory.FullName);
|
||||
if (moduleConfiguration?.Links?.ContainsKey(relativePath) ?? false)
|
||||
{
|
||||
filesToLink.Add(new ItemToLink(subDir.FullName, ResolvePath(moduleConfiguration.Links[relativePath], targetDirectory.FullName)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var subDirLinks = await TraverseTree(
|
||||
subDir,
|
||||
new DirectoryInfo(Path.Combine(currentTargetDirectory.FullName, subDir.Name)),
|
||||
moduleDirectory,
|
||||
targetDirectory,
|
||||
moduleConfiguration
|
||||
);
|
||||
subDirLinksToAdd = subDirLinksToAdd.Concat(subDirLinks);
|
||||
}
|
||||
}
|
||||
|
||||
return filesToLink.Concat(subDirLinksToAdd);
|
||||
}
|
||||
|
||||
private static string? GetRepositoryName(List<string> parameters)
|
||||
{
|
||||
//TODO: handle parameters
|
||||
if (parameters.Count < 2) return null;
|
||||
return parameters[1];
|
||||
}
|
||||
|
||||
private static string ResolvePath(string path, string currentDirectory)
|
||||
{
|
||||
if (path.StartsWith("~"))
|
||||
{
|
||||
path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), path.Substring(2));
|
||||
}
|
||||
|
||||
//TODO: more special character
|
||||
|
||||
return Path.Combine(currentDirectory, path);
|
||||
}
|
||||
|
||||
private static string GetRelativePath(string full, string parent) => full.Substring(parent.Length).TrimStart(Path.DirectorySeparatorChar);
|
||||
}
|
||||
90
src/Alma.App/Command/List/ListCommand.cs
Normal file
90
src/Alma.App/Command/List/ListCommand.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using Alma.Configuration.Repository;
|
||||
using Alma.Data;
|
||||
using Alma.Services;
|
||||
|
||||
namespace Alma.Command.List;
|
||||
|
||||
public class ListCommand : ICommand
|
||||
{
|
||||
private readonly IRepositoryConfiguration _repositoryConfiguration;
|
||||
private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
|
||||
public string CommandString => "ls";
|
||||
|
||||
public ListCommand(
|
||||
IRepositoryConfiguration repositoryConfiguration,
|
||||
IModuleConfigurationResolver moduleConfigurationResolver)
|
||||
{
|
||||
_repositoryConfiguration = repositoryConfiguration;
|
||||
_moduleConfigurationResolver = moduleConfigurationResolver;
|
||||
}
|
||||
|
||||
public async Task Run(List<string> parameters)
|
||||
{
|
||||
if (parameters.Count > 0)
|
||||
{
|
||||
await ListModulesByRepoName(parameters[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ListRepositories();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ListRepositories()
|
||||
{
|
||||
Console.WriteLine("Repositories:" + Environment.NewLine);
|
||||
foreach (var repository in _repositoryConfiguration.Configuration.Repositories)
|
||||
{
|
||||
Console.WriteLine(repository.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ListModulesByRepoName(string repositoryName)
|
||||
{
|
||||
var repo = _repositoryConfiguration.Configuration.Repositories.FirstOrDefault(r => r.Name == repositoryName);
|
||||
if (repo is null)
|
||||
{
|
||||
Console.WriteLine($"No repository found with name '{repositoryName}'");
|
||||
return;
|
||||
}
|
||||
|
||||
if (repo.RepositoryPath is null)
|
||||
{
|
||||
Console.WriteLine($"No repository path is specified in repository settings '{repositoryName}'");
|
||||
return;
|
||||
}
|
||||
|
||||
await ListModules(repo.RepositoryPath, repositoryName);
|
||||
}
|
||||
|
||||
private async Task ListModules(string repositoryPath, string repositoryName)
|
||||
{
|
||||
var repositoryDirectory = new DirectoryInfo(repositoryPath);
|
||||
var moduleDirectories = await TraverseRepositoryFolder(repositoryDirectory);
|
||||
|
||||
Console.WriteLine($"Modules in repository '{repositoryName}':" + Environment.NewLine);
|
||||
foreach (var modulePath in moduleDirectories)
|
||||
{
|
||||
Console.WriteLine(modulePath.FullName.Substring(repositoryDirectory.FullName.Length).Replace(Path.DirectorySeparatorChar, '/'));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<DirectoryInfo>> TraverseRepositoryFolder(DirectoryInfo currentDirectory)
|
||||
{
|
||||
var moduleConfigFileStub = Path.Combine(currentDirectory.FullName, Constants.ModuleConfigFileStub);
|
||||
var (moduleConfiguration, moduleConfigurationFile) = await _moduleConfigurationResolver.ResolveModuleConfiguration(moduleConfigFileStub);
|
||||
|
||||
var result = Enumerable.Empty<DirectoryInfo>();
|
||||
if (moduleConfigurationFile is not null)
|
||||
{
|
||||
result = new List<DirectoryInfo> {currentDirectory};
|
||||
}
|
||||
|
||||
foreach (var subDir in currentDirectory.GetDirectories())
|
||||
{
|
||||
result = result.Concat(await TraverseRepositoryFolder(subDir));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
10
src/Alma.App/Command/Unlink/UnlinkCommand.cs
Normal file
10
src/Alma.App/Command/Unlink/UnlinkCommand.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Alma.Command.Unlink;
|
||||
|
||||
public class UnlinkCommand : ICommand
|
||||
{
|
||||
public string CommandString => "unlink";
|
||||
public Task Run(List<string> parameters)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Alma.Services;
|
||||
|
||||
namespace Alma.Configuration.Repository;
|
||||
|
||||
public class RepositoryConfiguration : IRepositoryConfiguration
|
||||
{
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly ConfigurationFileReader _configurationFileReader;
|
||||
|
||||
public RepositoryConfigurationRoot Configuration { get; private set; } = new RepositoryConfigurationRoot(new List<RepositoryConfigurationEntry>());
|
||||
|
||||
public RepositoryConfiguration(IFolderService folderService, ConfigurationFileReader configurationFileReader)
|
||||
{
|
||||
_folderService = folderService;
|
||||
_configurationFileReader = configurationFileReader;
|
||||
}
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
if (_folderService.ConfigRoot is null)
|
||||
{
|
||||
Configuration = new RepositoryConfigurationRoot(new List<RepositoryConfigurationEntry>());
|
||||
return;
|
||||
}
|
||||
|
||||
var repoConfigFileNameStub = Path.Combine(_folderService.ConfigRoot, "repository");
|
||||
var (configuration, repoConfigFileName) = await _configurationFileReader.DeserializeAsync<RepositoryConfigurationRoot>(repoConfigFileNameStub);
|
||||
Configuration = configuration ?? new RepositoryConfigurationRoot(new List<RepositoryConfigurationEntry>());
|
||||
|
||||
foreach (var repositoryConfigurationEntry in Configuration.Repositories)
|
||||
{
|
||||
if (repositoryConfigurationEntry.Name is null)
|
||||
{
|
||||
Console.WriteLine($"Entry name is null in {repoConfigFileName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/Alma.App/Data/Repository/RepositoryFolder.cs
Normal file
6
src/Alma.App/Data/Repository/RepositoryFolder.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Alma.Data.Repository;
|
||||
|
||||
public class RepositoryFolder
|
||||
{
|
||||
|
||||
}
|
||||
19
src/Alma.App/Helper/MD5Helper.cs
Normal file
19
src/Alma.App/Helper/MD5Helper.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Alma.Helper;
|
||||
|
||||
public static class MD5Helper
|
||||
{
|
||||
public static string GetMD5Hash(string source)
|
||||
{
|
||||
using var md5Hasher = MD5.Create();
|
||||
var data = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(source));
|
||||
var sBuilder = new StringBuilder();
|
||||
for (var i = 0; i < data.Length; i++)
|
||||
{
|
||||
sBuilder.Append(data[i].ToString("x2"));
|
||||
}
|
||||
return sBuilder.ToString();
|
||||
}
|
||||
}
|
||||
21
src/Alma.App/Services/ConfigurationFileReader.cs
Normal file
21
src/Alma.App/Services/ConfigurationFileReader.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace Alma.Services;
|
||||
|
||||
public class ConfigurationFileReader
|
||||
{
|
||||
private readonly List<IConfigurationFileReader> _configurationFileReaders;
|
||||
|
||||
public ConfigurationFileReader(IEnumerable<IConfigurationFileReader> configurationFileReaders)
|
||||
{
|
||||
_configurationFileReaders = configurationFileReaders.ToList();
|
||||
}
|
||||
|
||||
public async Task<(T? Result, string? FileName)> DeserializeAsync<T>(string fileNameWithoutExtension, string? extension = null) where T : class
|
||||
{
|
||||
foreach (var configurationFileReader in _configurationFileReaders)
|
||||
{
|
||||
if (await configurationFileReader.DeserializeAsync<T>(fileNameWithoutExtension, extension) is {Result: { }} result) return result;
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
51
src/Alma.App/Services/FolderService.cs
Normal file
51
src/Alma.App/Services/FolderService.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace Alma.Services;
|
||||
|
||||
public class FolderService : IFolderService
|
||||
{
|
||||
public string? ConfigRoot { get; }
|
||||
public string AppData { get; }
|
||||
|
||||
public FolderService()
|
||||
{
|
||||
ConfigRoot = GetConfigHomePath();
|
||||
AppData = GetAppDataPath();
|
||||
|
||||
if (!Directory.Exists(AppData)) Directory.CreateDirectory(AppData);
|
||||
}
|
||||
|
||||
private static string? GetConfigHomePath()
|
||||
{
|
||||
var configHomeProviders = new List<Func<string?>>
|
||||
{
|
||||
() => Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"),
|
||||
() => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config")
|
||||
};
|
||||
|
||||
var configHome = EnumerateProviders(configHomeProviders);
|
||||
return configHome == null ? null : Path.Combine(configHome, "alma");
|
||||
}
|
||||
|
||||
private static string GetAppDataPath()
|
||||
{
|
||||
var appDataProviders = new List<Func<string?>>
|
||||
{
|
||||
() => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
|
||||
};
|
||||
|
||||
var appData = EnumerateProviders(appDataProviders) ?? Environment.CurrentDirectory;
|
||||
return Path.Combine(appData, "alma");
|
||||
}
|
||||
|
||||
private static string? EnumerateProviders(List<Func<string?>> providers)
|
||||
{
|
||||
string? result = null;
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
result = provider();
|
||||
if (result is not null && Directory.Exists(result)) break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
18
src/Alma.App/Services/JsonConfigurationFileReader.cs
Normal file
18
src/Alma.App/Services/JsonConfigurationFileReader.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Alma.Services;
|
||||
|
||||
public class JsonConfigurationFileReader : IConfigurationFileReader
|
||||
{
|
||||
private static readonly JsonSerializerOptions DefaultOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task<(T? Result, string? FileName)> DeserializeAsync<T>(string fileNameWithoutExtension, string? extension) where T : class
|
||||
{
|
||||
extension ??= "json";
|
||||
var fileName = fileNameWithoutExtension + "." + extension;
|
||||
if (!File.Exists(fileName)) return (null, null);
|
||||
|
||||
await using FileStream openStream = File.OpenRead(fileName);
|
||||
return (await JsonSerializer.DeserializeAsync<T>(openStream, DefaultOptions), fileName);
|
||||
}
|
||||
}
|
||||
66
src/Alma.App/Services/MetadataHandler.cs
Normal file
66
src/Alma.App/Services/MetadataHandler.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.Text;
|
||||
using Alma.Data;
|
||||
using Alma.Helper;
|
||||
|
||||
namespace Alma.Services;
|
||||
|
||||
public class MetadataHandler : IMetadataHandler
|
||||
{
|
||||
private const string MetadataFilename = "moduleHashes.txt";
|
||||
private readonly IFolderService _folderService;
|
||||
|
||||
public MetadataHandler(IFolderService folderService)
|
||||
{
|
||||
_folderService = folderService;
|
||||
}
|
||||
|
||||
public async Task SaveLinkedItemsAsync(List<ItemToLink> successfulLinks, DirectoryInfo sourceDirectory, DirectoryInfo targetDirectory)
|
||||
{
|
||||
var sourcePathHash = MD5Helper.GetMD5Hash(sourceDirectory.FullName);
|
||||
var targetPathHash = MD5Helper.GetMD5Hash(targetDirectory.FullName);
|
||||
var modulePathHash = MD5Helper.GetMD5Hash(sourcePathHash + targetPathHash);
|
||||
var appDataDirectory = new DirectoryInfo(_folderService.AppData);
|
||||
|
||||
var moduleFolderMetadataPath = Path.Combine(appDataDirectory.FullName, modulePathHash + ".txt");
|
||||
|
||||
var previousData = new List<string>();
|
||||
|
||||
if (File.Exists(moduleFolderMetadataPath))
|
||||
{
|
||||
var content = await File.ReadAllLinesAsync(moduleFolderMetadataPath);
|
||||
previousData.AddRange(content.Skip(1));
|
||||
}
|
||||
|
||||
var newContent = previousData.Concat(successfulLinks.Select(s => s.TargetPath)).Distinct();
|
||||
await File.WriteAllLinesAsync(moduleFolderMetadataPath, newContent.Prepend("text"));
|
||||
|
||||
//TODO write md5 & path to a common file
|
||||
var hashAlreadySaved = false;
|
||||
|
||||
var metadataFilePath = Path.Combine(_folderService.AppData, MetadataFilename);
|
||||
if (File.Exists(metadataFilePath))
|
||||
{
|
||||
await using var metadataFileStream = File.OpenRead(metadataFilePath);
|
||||
using var metadataFileStream2 = new StreamReader(metadataFileStream);
|
||||
while (await metadataFileStream2.ReadLineAsync() is { } s)
|
||||
{
|
||||
if (!s.StartsWith(modulePathHash)) continue;
|
||||
|
||||
hashAlreadySaved = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hashAlreadySaved)
|
||||
{
|
||||
var newLineContent = modulePathHash + " " + EncodePath(sourceDirectory.FullName) + " " + EncodePath(targetDirectory.FullName);
|
||||
await File.AppendAllLinesAsync(metadataFilePath, new[] {newLineContent});
|
||||
}
|
||||
|
||||
static string EncodePath(string path)
|
||||
{
|
||||
byte[] toEncodeAsBytes = Encoding.UTF8.GetBytes(path);
|
||||
return Convert.ToBase64String(toEncodeAsBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Alma.App/Services/ModuleConfigurationResolver.cs
Normal file
35
src/Alma.App/Services/ModuleConfigurationResolver.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Alma.Configuration.Module;
|
||||
|
||||
namespace Alma.Services;
|
||||
|
||||
public class ModuleConfigurationResolver : IModuleConfigurationResolver
|
||||
{
|
||||
private readonly IConfigurationFileReader _configurationFileReader;
|
||||
private readonly IOsInformation _osInformation;
|
||||
|
||||
public ModuleConfigurationResolver(
|
||||
IConfigurationFileReader configurationFileReader,
|
||||
IOsInformation osInformation)
|
||||
{
|
||||
_configurationFileReader = configurationFileReader;
|
||||
_osInformation = osInformation;
|
||||
}
|
||||
|
||||
public async Task<(ModuleConfiguration? mergedModuleConfig, string? moduleConfigFileName)> ResolveModuleConfiguration(string moduleConfigStub)
|
||||
{
|
||||
var (moduleConfigRoot, moduleConfigFileName) = await _configurationFileReader.DeserializeAsync<ModuleConfigurationRoot>(moduleConfigStub);
|
||||
|
||||
if (moduleConfigRoot is null) return (null, null);
|
||||
|
||||
var validModuleConfigurations = moduleConfigRoot.Where(m => _osInformation.IsOnPlatform(m.Key));
|
||||
|
||||
//TODO: priority order
|
||||
var orderedValidModuleConfigurations = new Dictionary<string, ModuleConfiguration>(validModuleConfigurations);
|
||||
|
||||
var mergedModuleConfig = orderedValidModuleConfigurations
|
||||
.Select(m => m.Value)
|
||||
.Aggregate((a, b) => a.Merge(b));
|
||||
|
||||
return (mergedModuleConfig, moduleConfigFileName);
|
||||
}
|
||||
}
|
||||
24
src/Alma.App/Services/OsInformation.cs
Normal file
24
src/Alma.App/Services/OsInformation.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Alma.Services;
|
||||
|
||||
public class OsInformation : IOsInformation
|
||||
{
|
||||
private const string OsIdentifierDefault = "default";
|
||||
private const string OsIdentifierWin = "windows";
|
||||
private const string OsIdentifierLinux = "linux";
|
||||
|
||||
public string GetOsIdentifier()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return OsIdentifierWin;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return OsIdentifierLinux;
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
public bool IsOnPlatform(string platform)
|
||||
{
|
||||
if (platform == OsIdentifierDefault) return true;
|
||||
return platform == GetOsIdentifier();
|
||||
}
|
||||
}
|
||||
34
src/Alma.sln
Normal file
34
src/Alma.sln
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Alma.Abstraction", "Alma.Abstraction\Alma.Abstraction.csproj", "{49A2563D-8D89-4B2A-A81E-39A50F5B0C25}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Alma.App", "Alma.App\Alma.App.csproj", "{6FBB9920-A249-41AE-8CE2-5D2A4FF3B551}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Alma", "Alma\Alma.csproj", "{23157A6F-C737-4ED4-B36B-BFE3EA31EAF1}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{49A2563D-8D89-4B2A-A81E-39A50F5B0C25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{49A2563D-8D89-4B2A-A81E-39A50F5B0C25}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{49A2563D-8D89-4B2A-A81E-39A50F5B0C25}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{49A2563D-8D89-4B2A-A81E-39A50F5B0C25}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6FBB9920-A249-41AE-8CE2-5D2A4FF3B551}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6FBB9920-A249-41AE-8CE2-5D2A4FF3B551}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6FBB9920-A249-41AE-8CE2-5D2A4FF3B551}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6FBB9920-A249-41AE-8CE2-5D2A4FF3B551}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{23157A6F-C737-4ED4-B36B-BFE3EA31EAF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{23157A6F-C737-4ED4-B36B-BFE3EA31EAF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{23157A6F-C737-4ED4-B36B-BFE3EA31EAF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{23157A6F-C737-4ED4-B36B-BFE3EA31EAF1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
18
src/Alma/Alma.csproj
Normal file
18
src/Alma/Alma.csproj
Normal file
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Alma.App\Alma.App.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jab" Version="0.8.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
81
src/Alma/Program.cs
Normal file
81
src/Alma/Program.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Alma.Command;
|
||||
using Alma.Command.Help;
|
||||
using Alma.Command.Info;
|
||||
using Alma.Command.Link;
|
||||
using Alma.Command.List;
|
||||
using Alma.Command.Unlink;
|
||||
using Alma.Configuration.Repository;
|
||||
using Alma.Services;
|
||||
using Jab;
|
||||
|
||||
namespace Alma;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
/*public static async Task Main(string[] args)
|
||||
{
|
||||
var services = BuildServices();
|
||||
|
||||
var repositoryConfiguration = services.GetRequiredService<IRepositoryConfiguration>();
|
||||
await repositoryConfiguration.LoadAsync();
|
||||
var application = services.GetRequiredService<Application>();
|
||||
|
||||
await application.Run(args);
|
||||
|
||||
static IServiceProvider BuildServices()
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
|
||||
serviceCollection.AddSingleton<IRepositoryConfiguration, RepositoryConfiguration>();
|
||||
serviceCollection.AddSingleton<IFolderService, FolderService>();
|
||||
serviceCollection.AddSingleton<ConfigurationFileReader>();
|
||||
serviceCollection.AddSingleton<IConfigurationFileReader, JsonConfigurationFileReader>();
|
||||
serviceCollection.AddSingleton<IOsInformation, OsInformation>();
|
||||
serviceCollection.AddSingleton<ICommand, LinkCommand>();
|
||||
serviceCollection.AddSingleton<IModuleConfigurationResolver, ModuleConfigurationResolver>();
|
||||
serviceCollection.AddSingleton<Application>();
|
||||
|
||||
typeof(IRepositoryConfiguration), typeof(RepositoryConfiguration)
|
||||
typeof(IFolderService), typeof(FolderService)
|
||||
typeof(ConfigurationFileReader)
|
||||
typeof(IConfigurationFileReader), typeof(JsonConfigurationFileReader)
|
||||
typeof(IOsInformation), typeof(OsInformation)
|
||||
typeof(ICommand), typeof(LinkCommand)
|
||||
typeof(IModuleConfigurationResolver), typeof(ModuleConfigurationResolver)
|
||||
typeof(Application)
|
||||
|
||||
return serviceCollection.BuildServiceProvider();
|
||||
}
|
||||
}*/
|
||||
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
var services = new AlmaServiceProvider();
|
||||
|
||||
var repositoryConfiguration = services.GetService<IRepositoryConfiguration>();
|
||||
await repositoryConfiguration.LoadAsync();
|
||||
var application = services.GetService<Application>();
|
||||
|
||||
await application.Run(args);
|
||||
}
|
||||
}
|
||||
|
||||
[ServiceProvider]
|
||||
[Singleton(typeof(IRepositoryConfiguration), typeof(RepositoryConfiguration))]
|
||||
[Singleton(typeof(IFolderService), typeof(FolderService))]
|
||||
[Singleton(typeof(ConfigurationFileReader))]
|
||||
[Singleton(typeof(IConfigurationFileReader), typeof(JsonConfigurationFileReader))]
|
||||
[Singleton(typeof(IOsInformation), typeof(OsInformation))]
|
||||
[Singleton(typeof(ICommand), typeof(LinkCommand))]
|
||||
[Singleton(typeof(ICommand), typeof(UnlinkCommand))]
|
||||
[Singleton(typeof(ICommand), typeof(ModuleInfoCommand))]
|
||||
[Singleton(typeof(ICommand), typeof(ListCommand))]
|
||||
//Dependency cycle
|
||||
//[Singleton(typeof(ICommand), typeof(HelpCommand))]
|
||||
[Singleton(typeof(IModuleConfigurationResolver), typeof(ModuleConfigurationResolver))]
|
||||
[Singleton(typeof(IMetadataHandler), typeof(MetadataHandler))]
|
||||
[Singleton(typeof(Application))]
|
||||
internal partial class AlmaServiceProvider
|
||||
{
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user