diff --git a/.code_preloader.yml b/.code_preloader.yml index 97920cc..3e94732 100644 --- a/.code_preloader.yml +++ b/.code_preloader.yml @@ -8,15 +8,20 @@ # List of patterns to ignore during preloading ignore_list: - - ^\.git/ - - ^lib.* - - ^doc/ - ^bin/ + - ^\.code_preloader.yml + - ^doc/ + - ^\.drone.yml + - ^\.git/ + - ^\.gitattributes + - ^\.gitignore + - ^lib.* + - ^LICENSES/ - ^_prompts/ - ^\.reuse/ - - ^LICENSES/ - - ^\.vagrant/ - ^scripts/ + - ^\.tool-versions + - ^\.vagrant/ # Path to the output file (if null, output to STDOUT) output_path: null diff --git a/Makefile b/Makefile index f0c5bb8..f1faf70 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ test: install: install \ -m 755 \ - bin/code-preloader \ + bin/mfm \ $(PREFIX)/bin .PHONY: spec test build all prepare install diff --git a/README.md b/README.md index 254cb91..4186a45 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ > version of our project, please visit our primary repository at: > . + + # Minimalist Fuse Manager (MFM) MFM is a Crystal-lang CLI designed to streamline the management of various FUSE filesystems, such as sshfs, gocryptfs, httpdirfs, and more. Through its user-friendly interface, users can effortlessly mount and unmount filesystems, get real-time filesystem status, and handle errors proficiently. @@ -44,18 +46,23 @@ To build from source, you'll also need: For Debian/Ubuntu you can use the following command: ```shell-session -$ sudo apt-get update && sudo apt-get install libpcre3-dev libevent-2.1-dev +$ sudo apt-get update && sudo apt-get install libpcre3-dev libevent-2.1-dev make ``` ## Installation ### 1. From Source -1. Clone or download the source code. -2. Navigate to the source directory. -3. Run `shards install` to fetch dependencies. -4. Compile using `shards build`. -5. The compiled binary will be in the `bin` directory. +To get started with MFM, ensure that you have the prerequisites installed on your system (see above). + +Then follow these steps to install: + + git clone https://code.apps.glenux.net/glenux/mfm + cd mfm + make prepare + make build + sudo make install # either to install system-wide + make install PREFIX=$HOME/.local # or to install as a user ### 2. Binary Download diff --git a/shard.lock b/shard.lock index e19d81c..216e1db 100644 --- a/shard.lock +++ b/shard.lock @@ -4,6 +4,10 @@ shards: git: https://github.com/crystal-ameba/ameba.git version: 1.6.1 + baked_file_system: + git: https://github.com/schovi/baked_file_system.git + version: 0.10.0 + crinja: git: https://github.com/straight-shoota/crinja.git version: 0.8.1 diff --git a/shard.yml b/shard.yml index 3b29118..d65fad5 100644 --- a/shard.yml +++ b/shard.yml @@ -26,6 +26,9 @@ dependencies: github: hugopl/version_from_shard tablo: github: hutou/tablo + baked_file_system: + github: schovi/baked_file_system + version: 0.10.0 development_dependencies: ameba: diff --git a/src/commands/completion_autodetect.cr b/src/commands/completion_autodetect.cr new file mode 100644 index 0000000..8f413b8 --- /dev/null +++ b/src/commands/completion_autodetect.cr @@ -0,0 +1,17 @@ +require "./abstract_command" + +module GX::Commands + class CompletionAutodetect < AbstractCommand + def initialize(@config : GX::Config) + end + + def execute + STDERR.puts "FIXME: Completion auto-detection isn't implemented yet. Please select one of the following: --bash or --zsh" + exit(0) + end + + def self.handles_mode + GX::Types::Mode::CompletionAutodetect + end + end +end diff --git a/src/commands/completion_bash.cr b/src/commands/completion_bash.cr new file mode 100644 index 0000000..19c037c --- /dev/null +++ b/src/commands/completion_bash.cr @@ -0,0 +1,17 @@ +require "./abstract_command" + +module GX::Commands + class CompletionBash < AbstractCommand + def initialize(@config : GX::Config) + end + + def execute + completion_bash = FileStorage.get("completion.bash") + STDOUT.puts completion_bash.gets_to_end + end + + def self.handles_mode + GX::Types::Mode::CompletionBash + end + end +end diff --git a/src/commands/completion_zsh.cr b/src/commands/completion_zsh.cr new file mode 100644 index 0000000..f97c5ce --- /dev/null +++ b/src/commands/completion_zsh.cr @@ -0,0 +1,17 @@ +require "./abstract_command" + +module GX::Commands + class CompletionZsh < AbstractCommand + def initialize(@config : GX::Config) + end + + def execute + completion_bash = FileStorage.get("completion.zsh") + STDOUT.puts completion_bash.gets_to_end + end + + def self.handles_mode + GX::Types::Mode::CompletionZsh + end + end +end diff --git a/src/commands/config_init.cr b/src/commands/config_init.cr index b9407d3..07e5eb8 100644 --- a/src/commands/config_init.cr +++ b/src/commands/config_init.cr @@ -1,11 +1,36 @@ require "./abstract_command" +require "../file_storage" module GX::Commands class ConfigInit < AbstractCommand - def initialize(config : GX::Config) # FIXME + def initialize(@config : GX::Config) end def execute + config_dir = File.join(@config.home_dir, ".config", "mfm") + config_file_path = File.join(config_dir, "config.yml") + + # Guard condition to exit if the configuration file already exists + if File.exists?(config_file_path) + puts "Configuration file already exists at #{config_file_path}. No action taken." + return + end + + puts "Creating initial configuration file at #{config_file_path}" + + # Ensure the configuration directory exists + FileUtils.mkdir_p(config_dir) + + # Read the default configuration content from the baked file storage + default_config_content = FileStorage.get("sample.mfm.yaml") + + # Write the default configuration to the target path + File.write(config_file_path, default_config_content) + + puts "Configuration file created successfully." + rescue ex + STDERR.puts "Error creating the configuration file: #{ex.message}" + exit(1) end def self.handles_mode diff --git a/src/commands/global_completion.cr b/src/commands/global_completion.cr deleted file mode 100644 index d1be696..0000000 --- a/src/commands/global_completion.cr +++ /dev/null @@ -1,15 +0,0 @@ -require "./abstract_command" - -module GX::Commands - class GlobalCompletion < AbstractCommand - def initialize(@config : GX::Config) - end - - def execute - end - - def self.handles_mode - GX::Types::Mode::GlobalConfig - end - end -end diff --git a/src/commands/global_config.cr b/src/commands/global_config.cr index c369556..b3ff036 100644 --- a/src/commands/global_config.cr +++ b/src/commands/global_config.cr @@ -2,7 +2,7 @@ require "./abstract_command" module GX::Commands class GlobalConfig < AbstractCommand - def initialize(config : GX::Config) # FIXME + def initialize(config : GX::Config) end def execute diff --git a/src/commands/global_help.cr b/src/commands/global_help.cr index 78f1616..794ec2e 100644 --- a/src/commands/global_help.cr +++ b/src/commands/global_help.cr @@ -2,7 +2,7 @@ require "./abstract_command" module GX::Commands class GlobalHelp < AbstractCommand - def initialize(@config : GX::Config) # FIXME + def initialize(@config : GX::Config) end def execute diff --git a/src/commands/mapping_mount.cr b/src/commands/mapping_mount.cr index db81351..a750c7a 100644 --- a/src/commands/mapping_mount.cr +++ b/src/commands/mapping_mount.cr @@ -5,7 +5,7 @@ module GX::Commands class MappingMount < AbstractCommand @file_system_manager : FileSystemManager - def initialize(@config : GX::Config) # FIXME + def initialize(@config : GX::Config) @config.load_from_env @config.load_from_file @file_system_manager = FileSystemManager.new(@config) diff --git a/src/commands/mapping_umount.cr b/src/commands/mapping_umount.cr index dc61f1d..11dd92c 100644 --- a/src/commands/mapping_umount.cr +++ b/src/commands/mapping_umount.cr @@ -5,7 +5,7 @@ module GX::Commands class MappingUmount < AbstractCommand @file_system_manager : FileSystemManager - def initialize(@config : GX::Config) # FIXME + def initialize(@config : GX::Config) @config.load_from_env @config.load_from_file @file_system_manager = FileSystemManager.new(@config) diff --git a/src/file_storage.cr b/src/file_storage.cr new file mode 100644 index 0000000..dd01b59 --- /dev/null +++ b/src/file_storage.cr @@ -0,0 +1,8 @@ +require "baked_file_system" + +class FileStorage + extend BakedFileSystem + + bake_folder "../static" +end + diff --git a/src/file_system_manager.cr b/src/file_system_manager.cr index 91dc1a8..9dc003c 100644 --- a/src/file_system_manager.cr +++ b/src/file_system_manager.cr @@ -89,7 +89,9 @@ module GX end def choose_filesystem - names_display = {} of String => NamedTuple(filesystem: Models::AbstractFilesystemConfig, ansi_name: String) + names_display = {} of String => NamedTuple( + filesystem: Models::AbstractFilesystemConfig, + ansi_name: String) config_root = @config.root return if config_root.nil? @@ -114,16 +116,16 @@ module GX end # # FIXME: feat: allow to sort by name or by filesystem - sorted_values = names_display.values.sort_by { |item| item[:filesystem].name } + sorted_values = names_display.values.sort_by!(&.[:filesystem].name) result_filesystem_name = Utils::Fzf.run(sorted_values.map(&.[:ansi_name])).strip selected_filesystem = names_display[result_filesystem_name][:filesystem] puts ">> #{selected_filesystem.name}".colorize(:yellow) if !selected_filesystem - STDERR.puts "Vault not found: #{selected_filesystem}.".colorize(:red) + STDERR.puts "Mapping not found: #{selected_filesystem}.".colorize(:red) return end - return selected_filesystem + selected_filesystem end private def generate_display_name(filesystem : Models::AbstractFilesystemConfig) : String @@ -136,7 +138,7 @@ module GX if ENV["DISPLAY"]? || ENV["WAYLAND_DISPLAY"]? return true end - return false + false end end end diff --git a/src/main.cr b/src/main.cr index 28e57a4..baa8d14 100644 --- a/src/main.cr +++ b/src/main.cr @@ -22,7 +22,7 @@ struct BaseFormat < Log::StaticFormatter end Log.setup do |config| - backend = Log::IOBackend.new(formatter: BaseFormat) + backend = Log::IOBackend.new(io: STDERR, formatter: BaseFormat) config.bind "*", Log::Severity::Info, backend if ENV["LOG_LEVEL"]? diff --git a/src/models/concerns/base.cr b/src/models/concerns/base.cr index 90254de..219a456 100644 --- a/src/models/concerns/base.cr +++ b/src/models/concerns/base.cr @@ -31,7 +31,7 @@ module GX::Models::Concerns end end - def _mount_wrapper(&block) : Nil + def _mount_wrapper(&) : Nil mount_point_safe = mount_point return if mount_point_safe.nil? @@ -46,7 +46,7 @@ module GX::Models::Concerns if result_status.success? puts "Models #{name} is now available on #{mount_point_safe}".colorize(:green) else - puts "Error mounting the vault".colorize(:red) + puts "Error mounting the mapping".colorize(:red) return end end diff --git a/src/models/gocryptfs_config.cr b/src/models/gocryptfs_config.cr index 9013bdf..f6ea5b0 100644 --- a/src/models/gocryptfs_config.cr +++ b/src/models/gocryptfs_config.cr @@ -32,7 +32,7 @@ module GX::Models output: STDOUT, error: STDERR ) - return process.wait + process.wait end end end diff --git a/src/models/httpdirfs_config.cr b/src/models/httpdirfs_config.cr index 30db9eb..bcf0227 100644 --- a/src/models/httpdirfs_config.cr +++ b/src/models/httpdirfs_config.cr @@ -32,7 +32,7 @@ module GX::Models output: STDOUT, error: STDERR ) - return process.wait + process.wait end end end diff --git a/src/models/sshfs_config.cr b/src/models/sshfs_config.cr index 99da441..82b3538 100644 --- a/src/models/sshfs_config.cr +++ b/src/models/sshfs_config.cr @@ -39,7 +39,7 @@ module GX::Models output: STDOUT, error: STDERR ) - return process.wait + process.wait end end end diff --git a/src/parsers/completion_parser.cr b/src/parsers/completion_parser.cr index 1cdc4cc..e0d5abb 100644 --- a/src/parsers/completion_parser.cr +++ b/src/parsers/completion_parser.cr @@ -4,6 +4,7 @@ module GX::Parsers class CompletionParser < AbstractParser def build(parser, ancestors, config) breadcrumbs = ancestors + "completion" + # config.mode = Types::Mode::CompletionAutodetect parser.banner = Utils.usage_line( breadcrumbs, @@ -12,11 +13,13 @@ module GX::Parsers ) parser.separator("\nCompletion commands:") - parser.on("--bash", "Generate bash completion") do |flag| + parser.on("--bash", "Generate bash completion") do |_| + config.mode = Types::Mode::CompletionBash Log.info { "Set bash completion" } end - parser.on("--zsh", "Generate zsh completion") do |flag| + parser.on("--zsh", "Generate zsh completion") do |_| + config.mode = Types::Mode::CompletionZsh Log.info { "Set zsh completion" } end diff --git a/src/parsers/config_parser.cr b/src/parsers/config_parser.cr index b902cd5..f1dfa3f 100644 --- a/src/parsers/config_parser.cr +++ b/src/parsers/config_parser.cr @@ -23,7 +23,7 @@ module GX::Parsers parser.banner = Utils.usage_line(breadcrumbs + "init", "Create initial mfm configuration") parser.separator("\nInit options") - parser.on("-p", "--path", "Set vault encrypted path") do |path| + parser.on("-p", "--path", "Set mapping encrypted path") do |path| config.config_init_options.try do |opts| opts.path = path end diff --git a/src/parsers/mapping_parser.cr b/src/parsers/mapping_parser.cr index fc972a9..d39b99f 100644 --- a/src/parsers/mapping_parser.cr +++ b/src/parsers/mapping_parser.cr @@ -5,8 +5,10 @@ module GX::Parsers class MappingParser < AbstractParser def build(parser, ancestors, config) breadcrumbs = ancestors + "mapping" - add_args = {name: "", path: ""} + create_args = {name: "", path: ""} delete_args = {name: ""} + mount_args = {name: ""} + umount_args = {name: ""} parser.banner = Utils.usage_line( breadcrumbs, @@ -18,50 +20,94 @@ module GX::Parsers parser.on("list", "List mappings") do config.mode = Types::Mode::MappingList parser.separator(Utils.help_line(breadcrumbs + "list")) - # abort("FIXME: Not implemented") end parser.on("create", "Create mapping") do config.mode = Types::Mode::MappingCreate - - pp parser + # pp parser parser.banner = Utils.usage_line(breadcrumbs + "create", "Create mapping", true) parser.separator("\nCreate options") - parser.on("-n", "--name", "Set vault name") do |name| - add_args = add_args.merge({name: name}) + parser.on("-t", "--type TYPE", "Set filesystem type") do |type| + create_args = create_args.merge({type: type}) end - parser.on("-p", "--path", "Set vault encrypted path") do |path| - add_args = add_args.merge({path: path}) + parser.on("-n", "--name", "Set mapping name") do |name| + create_args = create_args.merge({name: name}) end + + # Filesystem specific + parser.on("--encrypted-path PATH", "Set encrypted path (for gocryptfs)") do |path| + encrypted_path = path + end + parser.on("--remote-user USER", "Set SSH user (for sshfs)") do |user| + create_args = create_args.merge({remote_user: user}) + end + parser.on("--remote-host HOST", "Set SSH host (for sshfs)") do |host| + create_args = create_args.merge({remote_host: host}) + end + parser.on("--source-path PATH", "Set remote path (for sshfs)") do |path| + create_args = create_args.merge({remote_path: path}) + end + parser.on("--remote-port PORT", "Set SSH port (for sshfs)") do |port| + create_args = create_args.merge({remote_port: port}) + end + parser.on("--url URL", "Set URL (for httpdirfs)") do |url| + create_args = create_args.merge({url: url}) + end + parser.separator(Utils.help_line(breadcrumbs + "create")) end - parser.on("edit", "Edit configuration") do |flag| + parser.on("edit", "Edit configuration") do |_| config.mode = Types::Mode::MappingEdit + + parser.on("--remote-user USER", "Set SSH user") do |user| + create_args = create_args.merge({remote_user: user}) + end + parser.on("--remote-host HOST", "Set SSH host") do |host| + create_args = create_args.merge({remote_host: host}) + end + parser.on("--source-path PATH", "Set remote path") do |path| + create_args = create_args.merge({remote_path: path}) + end + parser.separator(Utils.help_line(breadcrumbs + "edit")) # abort("FIXME: Not implemented") end - parser.on("mount", "Mount mapping") do |flag| + parser.on("mount", "Mount mapping") do |_| config.mode = Types::Mode::MappingMount + + parser.banner = Utils.usage_line(breadcrumbs + "mount", "mount mapping", true) + parser.separator("\nMount options") + + parser.on("-n", "--name", "Set mapping name") do |name| + mount_args = mount_args.merge({name: name}) + end + parser.separator(Utils.help_line(breadcrumbs + "mount")) - # abort("FIXME: Not implemented") end - parser.on("umount", "Umount mapping") do |flag| + parser.on("umount", "Umount mapping") do |_| config.mode = Types::Mode::MappingUmount + + parser.banner = Utils.usage_line(breadcrumbs + "umount", "umount mapping", true) + parser.separator("\nUmount options") + + parser.on("-n", "--name", "Set mapping name") do |name| + umount_args = umount_args.merge({name: name}) + end + parser.separator(Utils.help_line(breadcrumbs + "umount")) - # abort("FIXME: Not implemented") end parser.on("delete", "Delete mapping") do config.mode = Types::Mode::MappingDelete - parser.banner = Utils.usage_line(breadcrumbs + "delete", "Delete mapping", true) - parser.separator("\nDelete options") + parser.banner = Utils.usage_line(breadcrumbs + "delete", "delete mapping", true) + parser.separator("\ndelete options") - parser.on("-n", "--name", "Set vault name") do |name| + parser.on("-n", "--name", "Set mapping name") do |name| delete_args = delete_args.merge({name: name}) end parser.separator(Utils.help_line(breadcrumbs + "delete")) diff --git a/src/parsers/root_parser.cr b/src/parsers/root_parser.cr index 70ac5a5..67467d2 100644 --- a/src/parsers/root_parser.cr +++ b/src/parsers/root_parser.cr @@ -21,24 +21,24 @@ module GX::Parsers config.path = path end - parser.on("-v", "--verbose", "Set more verbosity") do |flag| + parser.on("-v", "--verbose", "Set more verbosity") do |_| Log.info { "Verbosity enabled" } config.verbose = true end - parser.on("-o", "--open", "Automatically open directory after mount") do |flag| + parser.on("-o", "--open", "Automatically open directory after mount") do |_| Log.info { "Auto-open enabled" } config.auto_open = true end - parser.on("--version", "Show version") do |flag| + parser.on("--version", "Show version") do |_| config.mode = Types::Mode::GlobalVersion end - parser.on("-h", "--help", "Show this help") do |flag| + parser.on("-h", "--help", "Show this help") do |_| config.mode = Types::Mode::GlobalHelp config.help_options = Parsers::Options::HelpOptions.new - config.help_options.try { |opts| opts.parser_snapshot = parser.dup } + config.help_options.try(&.parser_snapshot=(parser.dup)) end parser.separator("\nGlobal commands:") @@ -46,7 +46,7 @@ module GX::Parsers parser.on("config", "Manage configuration file") do config.mode = Types::Mode::GlobalHelp config.help_options = Parsers::Options::HelpOptions.new - config.help_options.try { |opts| opts.parser_snapshot = parser.dup } + config.help_options.try(&.parser_snapshot=(parser.dup)) # config.command = Commands::Config.new(config) Parsers::ConfigParser.new.build(parser, breadcrumbs, config) @@ -59,7 +59,7 @@ module GX::Parsers parser.on("mapping", "Manage mappings") do config.mode = Types::Mode::GlobalHelp config.help_options = Parsers::Options::HelpOptions.new - config.help_options.try { |opts| opts.parser_snapshot = parser.dup } + config.help_options.try(&.parser_snapshot=(parser.dup)) Parsers::MappingParser.new.build(parser, breadcrumbs, config) end @@ -69,7 +69,10 @@ module GX::Parsers # end parser.on("completion", "Manage completion") do - config.mode = Types::Mode::GlobalCompletion + config.mode = Types::Mode::GlobalHelp + config.help_options = Parsers::Options::HelpOptions.new + config.help_options.try(&.parser_snapshot=(parser.dup)) + Parsers::CompletionParser.new.build(parser, breadcrumbs, config) end diff --git a/src/types/modes.cr b/src/types/modes.cr index 6498f89..2454e41 100644 --- a/src/types/modes.cr +++ b/src/types/modes.cr @@ -4,11 +4,15 @@ module GX::Types GlobalVersion GlobalHelp - GlobalCompletion + # GlobalCompletion GlobalTui GlobalConfig GlobalMapping + CompletionBash + CompletionZsh + CompletionAutodetect + ConfigInit MappingCreate diff --git a/src/utils/breadcrumbs.cr b/src/utils/breadcrumbs.cr index 89c54e9..f12c5f8 100644 --- a/src/utils/breadcrumbs.cr +++ b/src/utils/breadcrumbs.cr @@ -4,12 +4,12 @@ module GX::Utils @ancestors = base end - def +(elem : String) - b = BreadCrumbs.new(@ancestors + [elem]) + def +(other : String) + BreadCrumbs.new(@ancestors + [other]) end - def to_s - @ancestors.join(" ") + def to_s(io : IO) + io << @ancestors.join(" ") end def to_a diff --git a/src/utils/parser_lines.cr b/src/utils/parser_lines.cr index dacd71f..4d9039e 100644 --- a/src/utils/parser_lines.cr +++ b/src/utils/parser_lines.cr @@ -3,7 +3,7 @@ require "./breadcrumbs" module GX::Utils def self.usage_line(breadcrumbs : BreadCrumbs, description : String, has_commands : Bool = false) [ - "Usage: #{breadcrumbs.to_s}#{has_commands ? " [commands]" : ""} [options]", + "Usage: #{breadcrumbs}#{has_commands ? " [commands]" : ""} [options]", "", description, "", @@ -12,6 +12,6 @@ module GX::Utils end def self.help_line(breadcrumbs : BreadCrumbs) - "\nRun '#{breadcrumbs.to_s} COMMAND --help' for more information on a command." + "\nRun '#{breadcrumbs} COMMAND --help' for more information on a command." end end diff --git a/static/completion.zsh b/static/completion.zsh new file mode 100644 index 0000000..89dd9f7 --- /dev/null +++ b/static/completion.zsh @@ -0,0 +1,5 @@ +#!/bin/zsh + +# mfm Zsh completion script + + diff --git a/static/sample.mfm.yaml b/static/sample.mfm.yaml new file mode 100644 index 0000000..8eaa2b5 --- /dev/null +++ b/static/sample.mfm.yaml @@ -0,0 +1,32 @@ +--- +version: 1 + +global: + mount_point_base: "{{env.HOME}}/mnt" + +filesystems: + ## + ## Sample configuration for encrypted vault (gocryptfs) + ## + # - type: gocryptfs + # name: "Credential Vault" + # encrypted_path: "{{env.HOME}}/Documents/Credential.Vault" + # + ## + ## Sample configuration remote SSH directory (sshfs) + ## + # - type: sshfs + # name: "Remote SSH server" + # remote_host: ssh.example.com + # remote_user: "{{env.USER}}" + # remote_path: "/home/{{env.USER}}" + # remote_port: 443 + # + ## + ## Sample configuration for remote HTTP directory (httpdirfs) + ## + - type: httpdirfs + name: "Debian Repository" + url: "http://ftp.debian.org/debian/" + # mount_point: "{{env.HOME}}/another.dir" +#