diff --git a/src/Alma.Abstraction/Alma.Abstraction.csproj b/src/Alma.Abstraction/Alma.Abstraction.csproj new file mode 100644 index 0000000..431d608 --- /dev/null +++ b/src/Alma.Abstraction/Alma.Abstraction.csproj @@ -0,0 +1,10 @@ + + + + net6.0 + enable + enable + Alma + + + diff --git a/src/Alma.Abstraction/Command/ICommand.cs b/src/Alma.Abstraction/Command/ICommand.cs new file mode 100644 index 0000000..3d0d2e4 --- /dev/null +++ b/src/Alma.Abstraction/Command/ICommand.cs @@ -0,0 +1,7 @@ +namespace Alma.Command; + +public interface ICommand +{ + public string CommandString { get; } + public Task Run(List parameters); +} \ No newline at end of file diff --git a/src/Alma.Abstraction/Configuration/Module/ModuleConfiguration.cs b/src/Alma.Abstraction/Configuration/Module/ModuleConfiguration.cs new file mode 100644 index 0000000..b8e9fbc --- /dev/null +++ b/src/Alma.Abstraction/Configuration/Module/ModuleConfiguration.cs @@ -0,0 +1,17 @@ +namespace Alma.Configuration.Module; + +public record ModuleConfiguration(string? Target, Dictionary? Links) +{ + public ModuleConfiguration Merge(ModuleConfiguration merge) + { + var mergedLinks = (Links ?? new Dictionary()) + .Concat(merge.Links ?? new Dictionary()); + return new ModuleConfiguration( + merge.Target ?? Target, + new Dictionary(mergedLinks) + ); + } + + public static ModuleConfiguration Empty() => + new(null, new Dictionary()); +} \ No newline at end of file diff --git a/src/Alma.Abstraction/Configuration/Module/ModuleConfigurationRoot.cs b/src/Alma.Abstraction/Configuration/Module/ModuleConfigurationRoot.cs new file mode 100644 index 0000000..6121119 --- /dev/null +++ b/src/Alma.Abstraction/Configuration/Module/ModuleConfigurationRoot.cs @@ -0,0 +1,5 @@ +namespace Alma.Configuration.Module; + +public class ModuleConfigurationRoot : Dictionary +{ +} \ No newline at end of file diff --git a/src/Alma.Abstraction/Configuration/Repository/IRepositoryConfiguration.cs b/src/Alma.Abstraction/Configuration/Repository/IRepositoryConfiguration.cs new file mode 100644 index 0000000..45c7a56 --- /dev/null +++ b/src/Alma.Abstraction/Configuration/Repository/IRepositoryConfiguration.cs @@ -0,0 +1,7 @@ +namespace Alma.Configuration.Repository; + +public interface IRepositoryConfiguration +{ + public Task LoadAsync(); + RepositoryConfigurationRoot Configuration { get; } +} \ No newline at end of file diff --git a/src/Alma.Abstraction/Configuration/Repository/RepositoryConfigurationEntry.cs b/src/Alma.Abstraction/Configuration/Repository/RepositoryConfigurationEntry.cs new file mode 100644 index 0000000..93bf6b7 --- /dev/null +++ b/src/Alma.Abstraction/Configuration/Repository/RepositoryConfigurationEntry.cs @@ -0,0 +1,7 @@ +namespace Alma.Configuration.Repository; + +public record RepositoryConfigurationEntry( + string Name, + string? RepositoryPath, + string? LinkPath +); \ No newline at end of file diff --git a/src/Alma.Abstraction/Configuration/Repository/RepositoryConfigurationRoot.cs b/src/Alma.Abstraction/Configuration/Repository/RepositoryConfigurationRoot.cs new file mode 100644 index 0000000..5f4fdb8 --- /dev/null +++ b/src/Alma.Abstraction/Configuration/Repository/RepositoryConfigurationRoot.cs @@ -0,0 +1,3 @@ +namespace Alma.Configuration.Repository; + +public record RepositoryConfigurationRoot(List Repositories); \ No newline at end of file diff --git a/src/Alma.Abstraction/Data/Constants.cs b/src/Alma.Abstraction/Data/Constants.cs new file mode 100644 index 0000000..4b0eaef --- /dev/null +++ b/src/Alma.Abstraction/Data/Constants.cs @@ -0,0 +1,6 @@ +namespace Alma.Data; + +public static class Constants +{ + public static readonly string ModuleConfigFileStub = ".alma-config"; +} \ No newline at end of file diff --git a/src/Alma.Abstraction/Data/ItemToLink.cs b/src/Alma.Abstraction/Data/ItemToLink.cs new file mode 100644 index 0000000..a2fc7e2 --- /dev/null +++ b/src/Alma.Abstraction/Data/ItemToLink.cs @@ -0,0 +1,3 @@ +namespace Alma.Data; + +public record ItemToLink(string SourcePath, string TargetPath); \ No newline at end of file diff --git a/src/Alma.Abstraction/Services/IConfigurationFileReader.cs b/src/Alma.Abstraction/Services/IConfigurationFileReader.cs new file mode 100644 index 0000000..5be72d5 --- /dev/null +++ b/src/Alma.Abstraction/Services/IConfigurationFileReader.cs @@ -0,0 +1,6 @@ +namespace Alma.Services; + +public interface IConfigurationFileReader +{ + public Task<(T? Result, string? FileName)> DeserializeAsync(string fileNameWithoutExtension, string? extension = null) where T : class; +} \ No newline at end of file diff --git a/src/Alma.Abstraction/Services/IFolderService.cs b/src/Alma.Abstraction/Services/IFolderService.cs new file mode 100644 index 0000000..80badd0 --- /dev/null +++ b/src/Alma.Abstraction/Services/IFolderService.cs @@ -0,0 +1,7 @@ +namespace Alma.Services; + +public interface IFolderService +{ + string? ConfigRoot { get; } + string AppData { get; } +} \ No newline at end of file diff --git a/src/Alma.Abstraction/Services/IMetadataHandler.cs b/src/Alma.Abstraction/Services/IMetadataHandler.cs new file mode 100644 index 0000000..64fa4e8 --- /dev/null +++ b/src/Alma.Abstraction/Services/IMetadataHandler.cs @@ -0,0 +1,8 @@ +using Alma.Data; + +namespace Alma.Services; + +public interface IMetadataHandler +{ + Task SaveLinkedItemsAsync(List successfulLinks, DirectoryInfo sourceDirectory, DirectoryInfo targetDirectory); +} \ No newline at end of file diff --git a/src/Alma.Abstraction/Services/IModuleConfigurationResolver.cs b/src/Alma.Abstraction/Services/IModuleConfigurationResolver.cs new file mode 100644 index 0000000..5508439 --- /dev/null +++ b/src/Alma.Abstraction/Services/IModuleConfigurationResolver.cs @@ -0,0 +1,8 @@ +using Alma.Configuration.Module; + +namespace Alma.Services; + +public interface IModuleConfigurationResolver +{ + Task<(ModuleConfiguration? mergedModuleConfig, string? moduleConfigFileName)> ResolveModuleConfiguration(string moduleConfigStub); +} \ No newline at end of file diff --git a/src/Alma.Abstraction/Services/IOsInformation.cs b/src/Alma.Abstraction/Services/IOsInformation.cs new file mode 100644 index 0000000..6dd7429 --- /dev/null +++ b/src/Alma.Abstraction/Services/IOsInformation.cs @@ -0,0 +1,7 @@ +namespace Alma.Services; + +public interface IOsInformation +{ + string GetOsIdentifier(); + bool IsOnPlatform(string platform); +} \ No newline at end of file diff --git a/src/Alma.App/Alma.App.csproj b/src/Alma.App/Alma.App.csproj new file mode 100644 index 0000000..8ffd592 --- /dev/null +++ b/src/Alma.App/Alma.App.csproj @@ -0,0 +1,14 @@ + + + + + + + + net6.0 + enable + enable + Alma + + + diff --git a/src/Alma.App/Application.cs b/src/Alma.App/Application.cs new file mode 100644 index 0000000..05e147b --- /dev/null +++ b/src/Alma.App/Application.cs @@ -0,0 +1,37 @@ +using Alma.Command; +using Alma.Command.Help; + +namespace Alma; + +public class Application +{ + private readonly IList _commands; + + public Application(IEnumerable 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; + } +} \ No newline at end of file diff --git a/src/Alma.App/Command/Help/HelpCommand.cs b/src/Alma.App/Command/Help/HelpCommand.cs new file mode 100644 index 0000000..8962af2 --- /dev/null +++ b/src/Alma.App/Command/Help/HelpCommand.cs @@ -0,0 +1,24 @@ +namespace Alma.Command.Help; + +public class HelpCommand : ICommand +{ + private readonly Func> _commandsProvider; + public string CommandString => "help"; + + public HelpCommand(Func> commandsProvider) + { + _commandsProvider = commandsProvider; + } + + public Task Run(List parameters) + { + Console.WriteLine("Commands:" + Environment.NewLine); + + foreach (var command in _commandsProvider().OrderBy(c => c.CommandString)) + { + Console.WriteLine(command.CommandString); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Alma.App/Command/Info/ModuleInfoCommand.cs b/src/Alma.App/Command/Info/ModuleInfoCommand.cs new file mode 100644 index 0000000..f2ca00b --- /dev/null +++ b/src/Alma.App/Command/Info/ModuleInfoCommand.cs @@ -0,0 +1,10 @@ +namespace Alma.Command.Info; + +public class ModuleInfoCommand : ICommand +{ + public string CommandString => "info"; + public Task Run(List parameters) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Alma.App/Command/Link/LinkCommand.cs b/src/Alma.App/Command/Link/LinkCommand.cs new file mode 100644 index 0000000..6ce2272 --- /dev/null +++ b/src/Alma.App/Command/Link/LinkCommand.cs @@ -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 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 CreateLinks(List itemsToLink) + { + var successfulLinks = new List(); + + 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> TraverseTree( + DirectoryInfo currentDirectory, + DirectoryInfo currentTargetDirectory, + DirectoryInfo moduleDirectory, + DirectoryInfo targetDirectory, + ModuleConfiguration? moduleConfiguration) + { + var filesToLink = new List(); + 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(); + + 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 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); +} \ No newline at end of file diff --git a/src/Alma.App/Command/List/ListCommand.cs b/src/Alma.App/Command/List/ListCommand.cs new file mode 100644 index 0000000..acc845a --- /dev/null +++ b/src/Alma.App/Command/List/ListCommand.cs @@ -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 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> TraverseRepositoryFolder(DirectoryInfo currentDirectory) + { + var moduleConfigFileStub = Path.Combine(currentDirectory.FullName, Constants.ModuleConfigFileStub); + var (moduleConfiguration, moduleConfigurationFile) = await _moduleConfigurationResolver.ResolveModuleConfiguration(moduleConfigFileStub); + + var result = Enumerable.Empty(); + if (moduleConfigurationFile is not null) + { + result = new List {currentDirectory}; + } + + foreach (var subDir in currentDirectory.GetDirectories()) + { + result = result.Concat(await TraverseRepositoryFolder(subDir)); + } + + return result; + } +} \ No newline at end of file diff --git a/src/Alma.App/Command/Unlink/UnlinkCommand.cs b/src/Alma.App/Command/Unlink/UnlinkCommand.cs new file mode 100644 index 0000000..48e97ef --- /dev/null +++ b/src/Alma.App/Command/Unlink/UnlinkCommand.cs @@ -0,0 +1,10 @@ +namespace Alma.Command.Unlink; + +public class UnlinkCommand : ICommand +{ + public string CommandString => "unlink"; + public Task Run(List parameters) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Alma.App/Configuration/Repository/RepositoryConfiguration.cs b/src/Alma.App/Configuration/Repository/RepositoryConfiguration.cs new file mode 100644 index 0000000..c6b29db --- /dev/null +++ b/src/Alma.App/Configuration/Repository/RepositoryConfiguration.cs @@ -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()); + + public RepositoryConfiguration(IFolderService folderService, ConfigurationFileReader configurationFileReader) + { + _folderService = folderService; + _configurationFileReader = configurationFileReader; + } + + public async Task LoadAsync() + { + if (_folderService.ConfigRoot is null) + { + Configuration = new RepositoryConfigurationRoot(new List()); + return; + } + + var repoConfigFileNameStub = Path.Combine(_folderService.ConfigRoot, "repository"); + var (configuration, repoConfigFileName) = await _configurationFileReader.DeserializeAsync(repoConfigFileNameStub); + Configuration = configuration ?? new RepositoryConfigurationRoot(new List()); + + foreach (var repositoryConfigurationEntry in Configuration.Repositories) + { + if (repositoryConfigurationEntry.Name is null) + { + Console.WriteLine($"Entry name is null in {repoConfigFileName}"); + } + } + } +} \ No newline at end of file diff --git a/src/Alma.App/Data/Repository/RepositoryFolder.cs b/src/Alma.App/Data/Repository/RepositoryFolder.cs new file mode 100644 index 0000000..c2c3e9d --- /dev/null +++ b/src/Alma.App/Data/Repository/RepositoryFolder.cs @@ -0,0 +1,6 @@ +namespace Alma.Data.Repository; + +public class RepositoryFolder +{ + +} \ No newline at end of file diff --git a/src/Alma.App/Helper/MD5Helper.cs b/src/Alma.App/Helper/MD5Helper.cs new file mode 100644 index 0000000..8d58c87 --- /dev/null +++ b/src/Alma.App/Helper/MD5Helper.cs @@ -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(); + } +} \ No newline at end of file diff --git a/src/Alma.App/Services/ConfigurationFileReader.cs b/src/Alma.App/Services/ConfigurationFileReader.cs new file mode 100644 index 0000000..100fd41 --- /dev/null +++ b/src/Alma.App/Services/ConfigurationFileReader.cs @@ -0,0 +1,21 @@ +namespace Alma.Services; + +public class ConfigurationFileReader +{ + private readonly List _configurationFileReaders; + + public ConfigurationFileReader(IEnumerable configurationFileReaders) + { + _configurationFileReaders = configurationFileReaders.ToList(); + } + + public async Task<(T? Result, string? FileName)> DeserializeAsync(string fileNameWithoutExtension, string? extension = null) where T : class + { + foreach (var configurationFileReader in _configurationFileReaders) + { + if (await configurationFileReader.DeserializeAsync(fileNameWithoutExtension, extension) is {Result: { }} result) return result; + } + + return (null, null); + } +} \ No newline at end of file diff --git a/src/Alma.App/Services/FolderService.cs b/src/Alma.App/Services/FolderService.cs new file mode 100644 index 0000000..af598b1 --- /dev/null +++ b/src/Alma.App/Services/FolderService.cs @@ -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> + { + () => 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> + { + () => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + }; + + var appData = EnumerateProviders(appDataProviders) ?? Environment.CurrentDirectory; + return Path.Combine(appData, "alma"); + } + + private static string? EnumerateProviders(List> providers) + { + string? result = null; + + foreach (var provider in providers) + { + result = provider(); + if (result is not null && Directory.Exists(result)) break; + } + + return result; + } +} \ No newline at end of file diff --git a/src/Alma.App/Services/JsonConfigurationFileReader.cs b/src/Alma.App/Services/JsonConfigurationFileReader.cs new file mode 100644 index 0000000..10d8a88 --- /dev/null +++ b/src/Alma.App/Services/JsonConfigurationFileReader.cs @@ -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(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(openStream, DefaultOptions), fileName); + } +} \ No newline at end of file diff --git a/src/Alma.App/Services/MetadataHandler.cs b/src/Alma.App/Services/MetadataHandler.cs new file mode 100644 index 0000000..17f98a1 --- /dev/null +++ b/src/Alma.App/Services/MetadataHandler.cs @@ -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 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(); + + 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); + } + } +} \ No newline at end of file diff --git a/src/Alma.App/Services/ModuleConfigurationResolver.cs b/src/Alma.App/Services/ModuleConfigurationResolver.cs new file mode 100644 index 0000000..265f035 --- /dev/null +++ b/src/Alma.App/Services/ModuleConfigurationResolver.cs @@ -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(moduleConfigStub); + + if (moduleConfigRoot is null) return (null, null); + + var validModuleConfigurations = moduleConfigRoot.Where(m => _osInformation.IsOnPlatform(m.Key)); + + //TODO: priority order + var orderedValidModuleConfigurations = new Dictionary(validModuleConfigurations); + + var mergedModuleConfig = orderedValidModuleConfigurations + .Select(m => m.Value) + .Aggregate((a, b) => a.Merge(b)); + + return (mergedModuleConfig, moduleConfigFileName); + } +} \ No newline at end of file diff --git a/src/Alma.App/Services/OsInformation.cs b/src/Alma.App/Services/OsInformation.cs new file mode 100644 index 0000000..931c1a6 --- /dev/null +++ b/src/Alma.App/Services/OsInformation.cs @@ -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(); + } +} \ No newline at end of file diff --git a/src/Alma.sln b/src/Alma.sln new file mode 100644 index 0000000..e3696d2 --- /dev/null +++ b/src/Alma.sln @@ -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 diff --git a/src/Alma/Alma.csproj b/src/Alma/Alma.csproj new file mode 100644 index 0000000..d6aad34 --- /dev/null +++ b/src/Alma/Alma.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + Exe + net6.0 + enable + enable + + + diff --git a/src/Alma/Program.cs b/src/Alma/Program.cs new file mode 100644 index 0000000..f4e6c4e --- /dev/null +++ b/src/Alma/Program.cs @@ -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(); + await repositoryConfiguration.LoadAsync(); + var application = services.GetRequiredService(); + + await application.Run(args); + + static IServiceProvider BuildServices() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + + 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(); + await repositoryConfiguration.LoadAsync(); + var application = services.GetService(); + + 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 +{ + +} \ No newline at end of file