From 283606c2801c1be515115478642918358efddf3c Mon Sep 17 00:00:00 2001 From: Glenn Date: Mon, 20 Nov 2023 12:37:31 +0100 Subject: [PATCH 01/32] fix: handle 'fusermount -u' return codes --- src/filesystems/filesystem.cr | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/filesystems/filesystem.cr b/src/filesystems/filesystem.cr index b5cf44d..da6090d 100644 --- a/src/filesystems/filesystem.cr +++ b/src/filesystems/filesystem.cr @@ -21,7 +21,13 @@ module GX module GenericFilesystem def unmount system("fusermount -u #{mount_dir.shellescape}") - puts "Filesystem #{name} is now closed.".colorize(:green) + fusermount_status = $? + + if fusermount_status.success? == 0 + puts "Filesystem #{name} is now closed.".colorize(:green) + else + puts "Error: Unable to unmount filesystem #{name} (exit code: #{fusermount_status.exit_code}).".colorize(:red) + end end def mount(&block) -- 2.45.2 From f05ee6b1b67176e8c4e57524a07a3f7d04281865 Mon Sep 17 00:00:00 2001 From: Glenn Date: Mon, 20 Nov 2023 12:40:13 +0100 Subject: [PATCH 02/32] fix: wrong comparison --- src/filesystems/filesystem.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filesystems/filesystem.cr b/src/filesystems/filesystem.cr index da6090d..8881bad 100644 --- a/src/filesystems/filesystem.cr +++ b/src/filesystems/filesystem.cr @@ -23,7 +23,7 @@ module GX system("fusermount -u #{mount_dir.shellescape}") fusermount_status = $? - if fusermount_status.success? == 0 + if fusermount_status.success? puts "Filesystem #{name} is now closed.".colorize(:green) else puts "Error: Unable to unmount filesystem #{name} (exit code: #{fusermount_status.exit_code}).".colorize(:red) -- 2.45.2 From d91e9a8fcd3554886c3730d5e7be01fc42215b2f Mon Sep 17 00:00:00 2001 From: glenux Date: Mon, 20 Nov 2023 15:45:40 +0000 Subject: [PATCH 03/32] Add examples for templating & disable non-implemented parts. --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bc58143..9fa994b 100644 --- a/README.md +++ b/README.md @@ -84,19 +84,19 @@ detail the filesystem names, types, and respective configurations. ### YAML File Format ```yaml -version: "1" - -global: - mountpoint: "/home/user/mnt/{{name}}" +# NOT IMPLEMENTED YET +# version: "1" +# global: +# mountpoint: "/home/user/mnt/{{filesystem.name}}" filesystems: - type: "gocryptfs" name: "Work - SSH Keys" - encrypted_path: "/home/user/.ssh/keyring.work" + encrypted_path: "/home/user/.ssh/keyring.work.vault" - type: "sshfs" name: "Personal - Media Server" - remote_user: "user" + remote_user: "{{env.USER}}" remote_host: "mediaserver.local" remote_path: "/mnt/largedisk/music" remote_port: 22 -- 2.45.2 From 8f2c2442a3ee24e2fcb2bcb4571e5ac536792006 Mon Sep 17 00:00:00 2001 From: Glenn Date: Tue, 21 Nov 2023 00:29:48 +0100 Subject: [PATCH 04/32] feat: add global.mountpoint and version parsing from YAML --- src/config.cr | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/config.cr b/src/config.cr index 913c066..311485f 100644 --- a/src/config.cr +++ b/src/config.cr @@ -25,6 +25,7 @@ module GX getter filesystems : Array(Filesystem) getter home_dir : String + getter global_mount_point : String? property verbose : Bool property mode : Mode property path : String? @@ -40,6 +41,7 @@ module GX @mode = Mode::Mount @filesystems = [] of Filesystem @path = nil + @global_mount_point = nil @args = NoArgs end @@ -82,14 +84,29 @@ module GX load_filesystems(path) end + # FIXME: render template on a value basis (instead of global) private def load_filesystems(config_path : String) + schema_version = nil file_data = File.read(config_path) - # FIXME: render template on a value basis (instead of global) file_patched = Crinja.render(file_data, {"env" => ENV.to_h}) yaml_data = YAML.parse(file_patched) - vaults_data = yaml_data["filesystems"].as_a + # Extract schema version + if yaml_data["version"]? + schema_version = yaml_data["version"].as_s? + end + + # Extract global settings + if yaml_data["global"]?.try &.as_h? + global_data = yaml_data["global"] + if global_data["mountpoint"]? + @global_mount_point = global_data["mountpoint"].as_s? + end + end + + # Extract filesystem data + vaults_data = yaml_data["filesystems"].as_a vaults_data.each do |filesystem_data| type = filesystem_data["type"].as_s name = filesystem_data["name"].as_s -- 2.45.2 From 994f9e1885a3188f808ff1416a87b74034a8a12b Mon Sep 17 00:00:00 2001 From: Glenn Date: Tue, 21 Nov 2023 00:30:59 +0100 Subject: [PATCH 05/32] refactor: use a better class hierarchy for filesystems --- src/cli.cr | 2 +- src/config.cr | 8 ++-- src/filesystems.cr | 2 +- src/filesystems/abstract_filesystem.cr | 50 +++++++++++++++++++++ src/filesystems/filesystem.cr | 48 --------------------- src/filesystems/gocryptfs.cr | 60 +++++++++++++------------- src/filesystems/httpdirfs.cr | 8 ++-- src/filesystems/sshfs.cr | 60 +++++++++++++------------- 8 files changed, 123 insertions(+), 115 deletions(-) create mode 100644 src/filesystems/abstract_filesystem.cr delete mode 100644 src/filesystems/filesystem.cr diff --git a/src/cli.cr b/src/cli.cr index f9a6578..f7ebb1d 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -97,7 +97,7 @@ module GX end def mount() - names_display = {} of String => NamedTuple(filesystem: Filesystem, ansi_name: String) + names_display = {} of String => NamedTuple(filesystem: Filesystem::AbstractFilesystem, ansi_name: String) @config.filesystems.each do |filesystem| fs_str = filesystem.type.ljust(12,' ') diff --git a/src/config.cr b/src/config.cr index 311485f..b35f6b7 100644 --- a/src/config.cr +++ b/src/config.cr @@ -23,7 +23,7 @@ module GX record AddArgs, name : String, path : String record DelArgs, name : String - getter filesystems : Array(Filesystem) + getter filesystems : Array(Filesystem::AbstractFilesystem) getter home_dir : String getter global_mount_point : String? property verbose : Bool @@ -39,7 +39,7 @@ module GX @verbose = false @mode = Mode::Mount - @filesystems = [] of Filesystem + @filesystems = [] of Filesystem::AbstractFilesystem @path = nil @global_mount_point = nil @@ -75,7 +75,7 @@ module GX path = detect_config_file() end @path = path - @filesystems = [] of Filesystem + @filesystems = [] of Filesystem::AbstractFilesystem if !File.exists? path Log.error { "File #{path} does not exist!".colorize(:red) } @@ -111,7 +111,7 @@ module GX type = filesystem_data["type"].as_s name = filesystem_data["name"].as_s # encrypted_path = filesystem_data["encrypted_path"].as_s - @filesystems << Filesystem.from_yaml(filesystem_data.to_yaml) + @filesystems << Filesystem::AbstractFilesystem.from_yaml(filesystem_data.to_yaml) # @filesystems << Filesystem.new(name, encrypted_path, "#{name}.Open") end end diff --git a/src/filesystems.cr b/src/filesystems.cr index 2026122..a8a1425 100644 --- a/src/filesystems.cr +++ b/src/filesystems.cr @@ -6,4 +6,4 @@ require "./filesystems/gocryptfs" require "./filesystems/sshfs" require "./filesystems/httpdirfs" -require "./filesystems/filesystem" +require "./filesystems/abstract_filesystem" diff --git a/src/filesystems/abstract_filesystem.cr b/src/filesystems/abstract_filesystem.cr new file mode 100644 index 0000000..443535d --- /dev/null +++ b/src/filesystems/abstract_filesystem.cr @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland +# Copyright © 2023 Glenn Y. Rolland + +require "yaml" + +module GX + module Filesystem + abstract class AbstractFilesystem + include YAML::Serializable + + use_yaml_discriminator "type", { + gocryptfs: GoCryptFS, + sshfs: SshFS, + httpdirfs: HttpDirFS + } + + property type : String + end + + module FilesystemBase + def unmount + system("fusermount -u #{mount_dir.shellescape}") + fusermount_status = $? + + if fusermount_status.success? + puts "Filesystem #{name} is now closed.".colorize(:green) + else + puts "Error: Unable to unmount filesystem #{name} (exit code: #{fusermount_status.exit_code}).".colorize(:red) + end + end + + def mount(&block) + Dir.mkdir_p(mount_dir) unless Dir.exists?(mount_dir) + if mounted? + puts "Already mounted. Skipping.".colorize(:yellow) + return + end + + yield + + puts "Filesystem #{name} is now available on #{mount_dir}".colorize(:green) + end + end + end +end + +require "./gocryptfs" +require "./sshfs" diff --git a/src/filesystems/filesystem.cr b/src/filesystems/filesystem.cr deleted file mode 100644 index 8881bad..0000000 --- a/src/filesystems/filesystem.cr +++ /dev/null @@ -1,48 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland -# Copyright © 2023 Glenn Y. Rolland - -require "yaml" - -module GX - abstract class Filesystem - include YAML::Serializable - - use_yaml_discriminator "type", { - gocryptfs: GoCryptFS, - sshfs: SshFS, - httpdirfs: HttpDirFS - } - - property type : String - end - - module GenericFilesystem - def unmount - system("fusermount -u #{mount_dir.shellescape}") - fusermount_status = $? - - if fusermount_status.success? - puts "Filesystem #{name} is now closed.".colorize(:green) - else - puts "Error: Unable to unmount filesystem #{name} (exit code: #{fusermount_status.exit_code}).".colorize(:red) - end - end - - def mount(&block) - Dir.mkdir_p(mount_dir) unless Dir.exists?(mount_dir) - if mounted? - puts "Already mounted. Skipping.".colorize(:yellow) - return - end - - yield - - puts "Filesystem #{name} is now available on #{mount_dir}".colorize(:green) - end - end -end - -require "./gocryptfs" -require "./sshfs" diff --git a/src/filesystems/gocryptfs.cr b/src/filesystems/gocryptfs.cr index ebe7f83..b405482 100644 --- a/src/filesystems/gocryptfs.cr +++ b/src/filesystems/gocryptfs.cr @@ -4,42 +4,44 @@ # Copyright © 2023 Glenn Y. Rolland require "shellwords" -require "./filesystem" +require "./abstract_filesystem" module GX - class GoCryptFS < Filesystem - getter name : String = "" - getter encrypted_path : String = "" + module Filesystem + class GoCryptFS < AbstractFilesystem + getter name : String = "" + getter encrypted_path : String = "" - @[YAML::Field(key: "mount_dir", ignore: true)] - getter mount_dir : String = "" + @[YAML::Field(key: "mount_dir", ignore: true)] + getter mount_dir : String = "" - include GenericFilesystem + include FilesystemBase - def after_initialize() - home_dir = ENV["HOME"] || raise "Home directory not found" - @mount_dir = File.join(home_dir, "mnt/#{@name}.Open") - end + def after_initialize() + home_dir = ENV["HOME"] || raise "Home directory not found" + @mount_dir = File.join(home_dir, "mnt/#{@name}.Open") + end - def mounted? : Bool - `mount`.includes?("#{encrypted_path} on #{mount_dir}") - end + def mounted? : Bool + `mount`.includes?("#{encrypted_path} on #{mount_dir}") + end - def mount - super do - input = STDIN - output = STDOUT - error = STDERR - process = Process.new( - "gocryptfs", - ["-idle", "15m", encrypted_path, mount_dir], - input: input, - output: output, - error: error - ) - unless process.wait.success? - puts "Error mounting the vault".colorize(:red) - return + def mount + super do + input = STDIN + output = STDOUT + error = STDERR + process = Process.new( + "gocryptfs", + ["-idle", "15m", encrypted_path, mount_dir], + input: input, + output: output, + error: error + ) + unless process.wait.success? + puts "Error mounting the vault".colorize(:red) + return + end end end end diff --git a/src/filesystems/httpdirfs.cr b/src/filesystems/httpdirfs.cr index e1e4279..481e4ba 100644 --- a/src/filesystems/httpdirfs.cr +++ b/src/filesystems/httpdirfs.cr @@ -4,17 +4,18 @@ # Copyright © 2023 Glenn Y. Rolland require "shellwords" -require "./filesystem" +require "./abstract_filesystem" module GX - class HttpDirFS < Filesystem + module Filesystem + class HttpDirFS < AbstractFilesystem getter name : String = "" getter url : String = "" @[YAML::Field(key: "mount_dir", ignore: true)] getter mount_dir : String = "" - include GenericFilesystem + include FilesystemBase def after_initialize() home_dir = ENV["HOME"] || raise "Home directory not found" @@ -44,5 +45,6 @@ module GX end end end + end end diff --git a/src/filesystems/sshfs.cr b/src/filesystems/sshfs.cr index 44ddab9..16ca006 100644 --- a/src/filesystems/sshfs.cr +++ b/src/filesystems/sshfs.cr @@ -4,38 +4,39 @@ # Copyright © 2023 Glenn Y. Rolland require "shellwords" -require "./filesystem" +require "./abstract_filesystem" module GX - class SshFS < Filesystem - getter name : String = "" - getter remote_path : String = "" - getter remote_user : String = "" - getter remote_host : String = "" - getter remote_port : String = "22" + module Filesystem + class SshFS < AbstractFilesystem + getter name : String = "" + getter remote_path : String = "" + getter remote_user : String = "" + getter remote_host : String = "" + getter remote_port : String = "22" - @[YAML::Field(key: "mount_dir", ignore: true)] - getter mount_dir : String = "" + @[YAML::Field(key: "mount_dir", ignore: true)] + getter mount_dir : String = "" - include GenericFilesystem + include FilesystemBase - def after_initialize() - home_dir = ENV["HOME"] || raise "Home directory not found" - @mount_dir = File.join(home_dir, "mnt/#{@name}") - end + def after_initialize() + home_dir = ENV["HOME"] || raise "Home directory not found" + @mount_dir = File.join(home_dir, "mnt/#{@name}") + end - def mounted? : Bool - `mount`.includes?("#{remote_user}@#{remote_host}:#{remote_path} on #{mount_dir}") - end + def mounted? : Bool + `mount`.includes?("#{remote_user}@#{remote_host}:#{remote_path} on #{mount_dir}") + end - def mount - super do - input = STDIN - output = STDOUT - error = STDERR - process = Process.new( - "sshfs", - [ + def mount + super do + input = STDIN + output = STDOUT + error = STDERR + process = Process.new( + "sshfs", + [ "-p", remote_port, "#{remote_user}@#{remote_host}:#{remote_path}", mount_dir @@ -43,10 +44,11 @@ module GX input: input, output: output, error: error - ) - unless process.wait.success? - puts "Error mounting the filesystem".colorize(:red) - return + ) + unless process.wait.success? + puts "Error mounting the filesystem".colorize(:red) + return + end end end end -- 2.45.2 From 587bff04caf4bccefd21b298d5311fa74f2240b1 Mon Sep 17 00:00:00 2001 From: Glenn Date: Tue, 21 Nov 2023 00:33:37 +0100 Subject: [PATCH 06/32] chore: pin crystal version with tool-versions --- .tool-versions | 1 + 1 file changed, 1 insertion(+) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..73cb02e --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +crystal 1.10.1 -- 2.45.2 From ee3f57ec2011c5ad8b3d8c7a2b49a5e4c042e7a1 Mon Sep 17 00:00:00 2001 From: Glenn Date: Tue, 21 Nov 2023 23:11:21 +0100 Subject: [PATCH 07/32] refactor: define abstract defs & move most functions to concerns/base --- src/filesystems/abstract_filesystem.cr | 29 ++---------------- src/filesystems/concerns/base.cr | 41 ++++++++++++++++++++++++++ src/filesystems/gocryptfs.cr | 23 +++++---------- src/filesystems/httpdirfs.cr | 23 +++++---------- src/filesystems/sshfs.cr | 25 ++++++---------- 5 files changed, 69 insertions(+), 72 deletions(-) create mode 100644 src/filesystems/concerns/base.cr diff --git a/src/filesystems/abstract_filesystem.cr b/src/filesystems/abstract_filesystem.cr index 443535d..29af3f8 100644 --- a/src/filesystems/abstract_filesystem.cr +++ b/src/filesystems/abstract_filesystem.cr @@ -17,34 +17,11 @@ module GX } property type : String - end - module FilesystemBase - def unmount - system("fusermount -u #{mount_dir.shellescape}") - fusermount_status = $? - - if fusermount_status.success? - puts "Filesystem #{name} is now closed.".colorize(:green) - else - puts "Error: Unable to unmount filesystem #{name} (exit code: #{fusermount_status.exit_code}).".colorize(:red) - end - end - - def mount(&block) - Dir.mkdir_p(mount_dir) unless Dir.exists?(mount_dir) - if mounted? - puts "Already mounted. Skipping.".colorize(:yellow) - return - end - - yield - - puts "Filesystem #{name} is now available on #{mount_dir}".colorize(:green) - end + abstract def mount() + abstract def unmount() + abstract def mounted_prefix() end end end -require "./gocryptfs" -require "./sshfs" diff --git a/src/filesystems/concerns/base.cr b/src/filesystems/concerns/base.cr new file mode 100644 index 0000000..ac0cd17 --- /dev/null +++ b/src/filesystems/concerns/base.cr @@ -0,0 +1,41 @@ + +module GX::Filesystem::Concerns + module Base + def after_initialize() + home_dir = ENV["HOME"] || raise "Home directory not found" + + # Use default mountpoint if none defined + if @mount_dir.empty? + @mount_dir = File.join(home_dir, "mnt/#{@name}") + end + end + + def mounted? : Bool + `mount`.includes?(" on #{mount_dir} type ") + end + + def unmount : Nil + system("fusermount -u #{mount_dir.shellescape}") + fusermount_status = $? + + if fusermount_status.success? + puts "Filesystem #{name} is now closed.".colorize(:green) + else + puts "Error: Unable to unmount filesystem #{name} (exit code: #{fusermount_status.exit_code}).".colorize(:red) + end + end + + def _mount_wrapper(&block) : Nil + Dir.mkdir_p(mount_dir) unless Dir.exists?(mount_dir) + if mounted? + puts "Already mounted. Skipping.".colorize(:yellow) + return + end + + yield + + puts "Filesystem #{name} is now available on #{mount_dir}".colorize(:green) + end + end + +end diff --git a/src/filesystems/gocryptfs.cr b/src/filesystems/gocryptfs.cr index b405482..3e5e4a1 100644 --- a/src/filesystems/gocryptfs.cr +++ b/src/filesystems/gocryptfs.cr @@ -5,6 +5,7 @@ require "shellwords" require "./abstract_filesystem" +require "./concerns/base" module GX module Filesystem @@ -15,28 +16,20 @@ module GX @[YAML::Field(key: "mount_dir", ignore: true)] getter mount_dir : String = "" - include FilesystemBase + include Concerns::Base - def after_initialize() - home_dir = ENV["HOME"] || raise "Home directory not found" - @mount_dir = File.join(home_dir, "mnt/#{@name}.Open") - end - - def mounted? : Bool - `mount`.includes?("#{encrypted_path} on #{mount_dir}") + def mounted_prefix() + "#{encrypted_path}" end def mount - super do - input = STDIN - output = STDOUT - error = STDERR + _mount_wrapper do process = Process.new( "gocryptfs", ["-idle", "15m", encrypted_path, mount_dir], - input: input, - output: output, - error: error + input: STDIN, + output: STDOUT, + error: STDERR ) unless process.wait.success? puts "Error mounting the vault".colorize(:red) diff --git a/src/filesystems/httpdirfs.cr b/src/filesystems/httpdirfs.cr index 481e4ba..16d7550 100644 --- a/src/filesystems/httpdirfs.cr +++ b/src/filesystems/httpdirfs.cr @@ -5,6 +5,7 @@ require "shellwords" require "./abstract_filesystem" +require "./concerns/base" module GX module Filesystem @@ -15,28 +16,20 @@ module GX @[YAML::Field(key: "mount_dir", ignore: true)] getter mount_dir : String = "" - include FilesystemBase + include Concerns::Base - def after_initialize() - home_dir = ENV["HOME"] || raise "Home directory not found" - @mount_dir = File.join(home_dir, "mnt/#{@name}") - end - - def mounted? : Bool - `mount`.includes?("httpdirfs on #{mount_dir}") + def mounted_prefix() + "httpdirfs" end def mount - super do - input = STDIN - output = STDOUT - error = STDERR + _mount_wrapper do process = Process.new( "httpdirfs", ["#{url}", mount_dir], - input: input, - output: output, - error: error + input: STDIN, + output: STDOUT, + error: STDERR ) unless process.wait.success? puts "Error mounting the filesystem".colorize(:red) diff --git a/src/filesystems/sshfs.cr b/src/filesystems/sshfs.cr index 16ca006..7f445ae 100644 --- a/src/filesystems/sshfs.cr +++ b/src/filesystems/sshfs.cr @@ -5,6 +5,7 @@ require "shellwords" require "./abstract_filesystem" +require "./concerns/base" module GX module Filesystem @@ -18,22 +19,14 @@ module GX @[YAML::Field(key: "mount_dir", ignore: true)] getter mount_dir : String = "" - include FilesystemBase + include Concerns::Base - def after_initialize() - home_dir = ENV["HOME"] || raise "Home directory not found" - @mount_dir = File.join(home_dir, "mnt/#{@name}") + def mounted_prefix() + "#{remote_user}@#{remote_host}:#{remote_path}" end - def mounted? : Bool - `mount`.includes?("#{remote_user}@#{remote_host}:#{remote_path} on #{mount_dir}") - end - - def mount - super do - input = STDIN - output = STDOUT - error = STDERR + def mount() + _mount_wrapper do process = Process.new( "sshfs", [ @@ -41,9 +34,9 @@ module GX "#{remote_user}@#{remote_host}:#{remote_path}", mount_dir ], - input: input, - output: output, - error: error + input: STDIN, + output: STDOUT, + error: STDERR ) unless process.wait.success? puts "Error mounting the filesystem".colorize(:red) -- 2.45.2 From 23d4def2170cfd7a4db0c0e4b07552075643c60f Mon Sep 17 00:00:00 2001 From: Glenn Date: Fri, 24 Nov 2023 00:20:16 +0100 Subject: [PATCH 08/32] feat: implement local & global mount_point definition --- src/cli.cr | 29 +++++++++--- src/config.cr | 56 ++++++++---------------- src/filesystems.cr | 9 ---- src/filesystems/abstract_filesystem.cr | 27 ------------ src/filesystems/concerns/base.cr | 41 ----------------- src/filesystems/gocryptfs.cr | 42 ------------------ src/filesystems/httpdirfs.cr | 43 ------------------ src/filesystems/sshfs.cr | 49 --------------------- src/main.cr | 2 - src/models.cr | 11 +++++ src/models/abstract_filesystem_config.cr | 32 ++++++++++++++ src/models/concerns/base.cr | 51 +++++++++++++++++++++ src/models/global_config.cr | 28 ++++++++++++ src/models/gocryptfs_config.cr | 41 +++++++++++++++++ src/models/httpdirfs_config.cr | 41 +++++++++++++++++ src/models/root_config.cr | 41 +++++++++++++++++ src/models/sshfs_config.cr | 48 ++++++++++++++++++++ 17 files changed, 334 insertions(+), 257 deletions(-) delete mode 100644 src/filesystems.cr delete mode 100644 src/filesystems/abstract_filesystem.cr delete mode 100644 src/filesystems/concerns/base.cr delete mode 100644 src/filesystems/gocryptfs.cr delete mode 100644 src/filesystems/httpdirfs.cr delete mode 100644 src/filesystems/sshfs.cr create mode 100644 src/models.cr create mode 100644 src/models/abstract_filesystem_config.cr create mode 100644 src/models/concerns/base.cr create mode 100644 src/models/global_config.cr create mode 100644 src/models/gocryptfs_config.cr create mode 100644 src/models/httpdirfs_config.cr create mode 100644 src/models/root_config.cr create mode 100644 src/models/sshfs_config.cr diff --git a/src/cli.cr b/src/cli.cr index f7ebb1d..844fa91 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -92,13 +92,18 @@ module GX STDOUT.puts "#{PROGRAM_NAME} #{VERSION}" when Config::Mode::Mount @config.load_from_file - mount + filesystem = choose_filesystem + mount_or_umount(filesystem) if !filesystem.nil? end end - def mount() - names_display = {} of String => NamedTuple(filesystem: Filesystem::AbstractFilesystem, ansi_name: String) - @config.filesystems.each do |filesystem| + def choose_filesystem() + names_display = {} of String => NamedTuple(filesystem: Models::AbstractFilesystemConfig, ansi_name: String) + + config_root = @config.root + return if config_root.nil? + + config_root.filesystems.each do |filesystem| fs_str = filesystem.type.ljust(12,' ') suffix = "" @@ -121,12 +126,22 @@ module GX selected_filesystem = names_display[result_filesystem_name][:filesystem] puts ">> #{selected_filesystem.name}".colorize(:yellow) - if selected_filesystem - selected_filesystem.mounted? ? selected_filesystem.unmount : selected_filesystem.mount - else + if !selected_filesystem STDERR.puts "Vault not found: #{selected_filesystem}.".colorize(:red) + return end + return selected_filesystem + end + def mount_or_umount(selected_filesystem) + config_root_safe = @config.root + return if config_root_safe.nil? + + if !selected_filesystem.mounted? + selected_filesystem.mount() + else + selected_filesystem.umount() + end end end end diff --git a/src/config.cr b/src/config.cr index b35f6b7..c936b77 100644 --- a/src/config.cr +++ b/src/config.cr @@ -5,7 +5,7 @@ require "crinja" -require "./filesystems" +require "./models" module GX class Config @@ -23,9 +23,10 @@ module GX record AddArgs, name : String, path : String record DelArgs, name : String - getter filesystems : Array(Filesystem::AbstractFilesystem) + # getter filesystems : Array(Models::AbstractFilesystemConfig) getter home_dir : String - getter global_mount_point : String? + getter root : Models::RootConfig? + property verbose : Bool property mode : Mode property path : String? @@ -39,14 +40,13 @@ module GX @verbose = false @mode = Mode::Mount - @filesystems = [] of Filesystem::AbstractFilesystem + @filesystems = [] of Models::AbstractFilesystemConfig @path = nil - @global_mount_point = nil @args = NoArgs end - def detect_config_file() + private def detect_config_file() possible_files = [ File.join(@home_dir, ".config", "mfm", "config.yaml"), File.join(@home_dir, ".config", "mfm", "config.yml"), @@ -70,50 +70,32 @@ module GX end def load_from_file - path = @path - if path.nil? - path = detect_config_file() + config_path = @path + if config_path.nil? + config_path = detect_config_file() end - @path = path - @filesystems = [] of Filesystem::AbstractFilesystem + @path = config_path - if !File.exists? path + if !File.exists? config_path Log.error { "File #{path} does not exist!".colorize(:red) } exit(1) end - load_filesystems(path) - end - # FIXME: render template on a value basis (instead of global) - private def load_filesystems(config_path : String) - schema_version = nil file_data = File.read(config_path) file_patched = Crinja.render(file_data, {"env" => ENV.to_h}) - yaml_data = YAML.parse(file_patched) + root = Models::RootConfig.from_yaml(file_patched) - # Extract schema version - if yaml_data["version"]? - schema_version = yaml_data["version"].as_s? - end + global_mount_point = root.global.mount_point + raise "Invalid global mount point" if global_mount_point.nil? - # Extract global settings - if yaml_data["global"]?.try &.as_h? - global_data = yaml_data["global"] - if global_data["mountpoint"]? - @global_mount_point = global_data["mountpoint"].as_s? + root.filesystems.each do |selected_filesystem| + if !selected_filesystem.mount_point? + selected_filesystem.mount_point = + File.join(global_mount_point, selected_filesystem.mounted_name) end end - - # Extract filesystem data - vaults_data = yaml_data["filesystems"].as_a - vaults_data.each do |filesystem_data| - type = filesystem_data["type"].as_s - name = filesystem_data["name"].as_s - # encrypted_path = filesystem_data["encrypted_path"].as_s - @filesystems << Filesystem::AbstractFilesystem.from_yaml(filesystem_data.to_yaml) - # @filesystems << Filesystem.new(name, encrypted_path, "#{name}.Open") - end + @root = root end end end diff --git a/src/filesystems.cr b/src/filesystems.cr deleted file mode 100644 index a8a1425..0000000 --- a/src/filesystems.cr +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland -# Copyright © 2023 Glenn Y. Rolland - -require "./filesystems/gocryptfs" -require "./filesystems/sshfs" -require "./filesystems/httpdirfs" -require "./filesystems/abstract_filesystem" diff --git a/src/filesystems/abstract_filesystem.cr b/src/filesystems/abstract_filesystem.cr deleted file mode 100644 index 29af3f8..0000000 --- a/src/filesystems/abstract_filesystem.cr +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland -# Copyright © 2023 Glenn Y. Rolland - -require "yaml" - -module GX - module Filesystem - abstract class AbstractFilesystem - include YAML::Serializable - - use_yaml_discriminator "type", { - gocryptfs: GoCryptFS, - sshfs: SshFS, - httpdirfs: HttpDirFS - } - - property type : String - - abstract def mount() - abstract def unmount() - abstract def mounted_prefix() - end - end -end - diff --git a/src/filesystems/concerns/base.cr b/src/filesystems/concerns/base.cr deleted file mode 100644 index ac0cd17..0000000 --- a/src/filesystems/concerns/base.cr +++ /dev/null @@ -1,41 +0,0 @@ - -module GX::Filesystem::Concerns - module Base - def after_initialize() - home_dir = ENV["HOME"] || raise "Home directory not found" - - # Use default mountpoint if none defined - if @mount_dir.empty? - @mount_dir = File.join(home_dir, "mnt/#{@name}") - end - end - - def mounted? : Bool - `mount`.includes?(" on #{mount_dir} type ") - end - - def unmount : Nil - system("fusermount -u #{mount_dir.shellescape}") - fusermount_status = $? - - if fusermount_status.success? - puts "Filesystem #{name} is now closed.".colorize(:green) - else - puts "Error: Unable to unmount filesystem #{name} (exit code: #{fusermount_status.exit_code}).".colorize(:red) - end - end - - def _mount_wrapper(&block) : Nil - Dir.mkdir_p(mount_dir) unless Dir.exists?(mount_dir) - if mounted? - puts "Already mounted. Skipping.".colorize(:yellow) - return - end - - yield - - puts "Filesystem #{name} is now available on #{mount_dir}".colorize(:green) - end - end - -end diff --git a/src/filesystems/gocryptfs.cr b/src/filesystems/gocryptfs.cr deleted file mode 100644 index 3e5e4a1..0000000 --- a/src/filesystems/gocryptfs.cr +++ /dev/null @@ -1,42 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland -# Copyright © 2023 Glenn Y. Rolland - -require "shellwords" -require "./abstract_filesystem" -require "./concerns/base" - -module GX - module Filesystem - class GoCryptFS < AbstractFilesystem - getter name : String = "" - getter encrypted_path : String = "" - - @[YAML::Field(key: "mount_dir", ignore: true)] - getter mount_dir : String = "" - - include Concerns::Base - - def mounted_prefix() - "#{encrypted_path}" - end - - def mount - _mount_wrapper do - process = Process.new( - "gocryptfs", - ["-idle", "15m", encrypted_path, mount_dir], - input: STDIN, - output: STDOUT, - error: STDERR - ) - unless process.wait.success? - puts "Error mounting the vault".colorize(:red) - return - end - end - end - end - end -end diff --git a/src/filesystems/httpdirfs.cr b/src/filesystems/httpdirfs.cr deleted file mode 100644 index 16d7550..0000000 --- a/src/filesystems/httpdirfs.cr +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland -# Copyright © 2023 Glenn Y. Rolland - -require "shellwords" -require "./abstract_filesystem" -require "./concerns/base" - -module GX - module Filesystem - class HttpDirFS < AbstractFilesystem - getter name : String = "" - getter url : String = "" - - @[YAML::Field(key: "mount_dir", ignore: true)] - getter mount_dir : String = "" - - include Concerns::Base - - def mounted_prefix() - "httpdirfs" - end - - def mount - _mount_wrapper do - process = Process.new( - "httpdirfs", - ["#{url}", mount_dir], - input: STDIN, - output: STDOUT, - error: STDERR - ) - unless process.wait.success? - puts "Error mounting the filesystem".colorize(:red) - return - end - end - end - end - end -end - diff --git a/src/filesystems/sshfs.cr b/src/filesystems/sshfs.cr deleted file mode 100644 index 7f445ae..0000000 --- a/src/filesystems/sshfs.cr +++ /dev/null @@ -1,49 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland -# Copyright © 2023 Glenn Y. Rolland - -require "shellwords" -require "./abstract_filesystem" -require "./concerns/base" - -module GX - module Filesystem - class SshFS < AbstractFilesystem - getter name : String = "" - getter remote_path : String = "" - getter remote_user : String = "" - getter remote_host : String = "" - getter remote_port : String = "22" - - @[YAML::Field(key: "mount_dir", ignore: true)] - getter mount_dir : String = "" - - include Concerns::Base - - def mounted_prefix() - "#{remote_user}@#{remote_host}:#{remote_path}" - end - - def mount() - _mount_wrapper do - process = Process.new( - "sshfs", - [ - "-p", remote_port, - "#{remote_user}@#{remote_host}:#{remote_path}", - mount_dir - ], - input: STDIN, - output: STDOUT, - error: STDERR - ) - unless process.wait.success? - puts "Error mounting the filesystem".colorize(:red) - return - end - end - end - end - end -end diff --git a/src/main.cr b/src/main.cr index f32183c..2fc1327 100644 --- a/src/main.cr +++ b/src/main.cr @@ -8,7 +8,6 @@ require "colorize" require "json" require "log" -require "./filesystems/gocryptfs" require "./config" require "./cli" @@ -32,7 +31,6 @@ Log.setup do |config| end end - app = GX::Cli.new app.parse_command_line(ARGV) app.run diff --git a/src/models.cr b/src/models.cr new file mode 100644 index 0000000..a5d97a4 --- /dev/null +++ b/src/models.cr @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland +# Copyright © 2023 Glenn Y. Rolland + +require "./models/root_config" +require "./models/global_config" +require "./models/gocryptfs_config" +require "./models/sshfs_config" +require "./models/httpdirfs_config" +require "./models/abstract_filesystem_config" diff --git a/src/models/abstract_filesystem_config.cr b/src/models/abstract_filesystem_config.cr new file mode 100644 index 0000000..d1006d8 --- /dev/null +++ b/src/models/abstract_filesystem_config.cr @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland +# Copyright © 2023 Glenn Y. Rolland + +require "yaml" + +module GX::Models + abstract class AbstractFilesystemConfig + include YAML::Serializable + # include YAML::Serializable::Strict + + use_yaml_discriminator "type", { + gocryptfs: GoCryptFSConfig, + sshfs: SshFSConfig, + httpdirfs: HttpDirFSConfig + } + + getter type : String + getter name : String + property mount_point : String? + + abstract def _mount_wrapper(&block) + abstract def _mount_action() + abstract def _mounted_prefix() + abstract def mounted_name() + abstract def mounted?() + abstract def mount() + abstract def umount() + abstract def mount_point?() + end +end diff --git a/src/models/concerns/base.cr b/src/models/concerns/base.cr new file mode 100644 index 0000000..9f0d656 --- /dev/null +++ b/src/models/concerns/base.cr @@ -0,0 +1,51 @@ + +module GX::Models::Concerns + module Base + def mounted?() : Bool + mount_point_safe = @mount_point + raise "Invalid mountpoint value" if mount_point_safe.nil? + + `mount`.includes?(" on #{mount_point_safe} type ") + end + + def umount() : Nil + mount_point_safe = @mount_point + raise "Invalid mountpoint value" if mount_point_safe.nil? + + system("fusermount -u #{mount_point_safe.shellescape}") + fusermount_status = $? + + if fusermount_status.success? + puts "Models #{name} is now closed.".colorize(:green) + else + puts "Error: Unable to unmount filesystem #{name} (exit code: #{fusermount_status.exit_code}).".colorize(:red) + end + end + + def mount_point?() + !mount_point.nil? + end + + def mount() + _mount_wrapper() do + _mount_action + end + end + + def _mount_wrapper(&block) : Nil + mount_point_safe = mount_point + return if mount_point_safe.nil? + + Dir.mkdir_p(mount_point_safe) unless Dir.exists?(mount_point_safe) + if mounted? + puts "Already mounted. Skipping.".colorize(:yellow) + return + end + + yield + + puts "Models #{name} is now available on #{mount_point_safe}".colorize(:green) + end + end + +end diff --git a/src/models/global_config.cr b/src/models/global_config.cr new file mode 100644 index 0000000..25759b5 --- /dev/null +++ b/src/models/global_config.cr @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland +# Copyright © 2023 Glenn Y. Rolland + +require "yaml" +require "./abstract_filesystem_config" + +module GX::Models + class GlobalConfig + include YAML::Serializable + include YAML::Serializable::Strict + + @[YAML::Field(key: "mount_point")] + getter mount_point : String? + + def after_initialize() + home_dir = ENV["HOME"] || raise "Home directory not found" + + # Set default mountpoint from global if none defined + if @mount_point.nil? || @mount_point.try &.empty? + @mount_point = File.join(home_dir, "mnt") + end + end + end +end + + diff --git a/src/models/gocryptfs_config.cr b/src/models/gocryptfs_config.cr new file mode 100644 index 0000000..2dd3cb8 --- /dev/null +++ b/src/models/gocryptfs_config.cr @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland +# Copyright © 2023 Glenn Y. Rolland + +require "shellwords" +require "./abstract_filesystem_config" +require "./concerns/base" + +module GX::Models + class GoCryptFSConfig < AbstractFilesystemConfig + getter encrypted_path : String = "" + + include Concerns::Base + + def _mounted_prefix() + "#{encrypted_path}" + end + + def mounted_name() + "#{@name}.Open" + end + + def _mount_action() + mount_point_safe = @mount_point + raise "Invalid mount point" if mount_point_safe.nil? + + process = Process.new( + "gocryptfs", + ["-idle", "15m", @encrypted_path, mount_point_safe], + input: STDIN, + output: STDOUT, + error: STDERR + ) + unless process.wait.success? + puts "Error mounting the vault".colorize(:red) + return + end + end + end +end diff --git a/src/models/httpdirfs_config.cr b/src/models/httpdirfs_config.cr new file mode 100644 index 0000000..29ab5dd --- /dev/null +++ b/src/models/httpdirfs_config.cr @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland +# Copyright © 2023 Glenn Y. Rolland + +require "shellwords" +require "./abstract_filesystem_config" +require "./concerns/base" + +module GX::Models + class HttpDirFSConfig < AbstractFilesystemConfig + getter url : String = "" + + include Concerns::Base + + def _mounted_prefix() + "httpdirfs" + end + + def mounted_name() + @name + end + + def _mount_action() + mount_point_safe = @mount_point + raise "Invalid mount point" if mount_point_safe.nil? + + process = Process.new( + "httpdirfs", + ["#{@url}", mount_point_safe], + input: STDIN, + output: STDOUT, + error: STDERR + ) + unless process.wait.success? + puts "Error mounting the filesystem".colorize(:red) + return + end + end + end +end diff --git a/src/models/root_config.cr b/src/models/root_config.cr new file mode 100644 index 0000000..c6de32b --- /dev/null +++ b/src/models/root_config.cr @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland +# Copyright © 2023 Glenn Y. Rolland + +require "yaml" +require "./abstract_filesystem_config" +require "./global_config" + +module GX::Models + # class CrinjaConverter + # def self.from_yaml(ctx : YAML::ParseContext , node : YAML::Nodes::Node) + # l_node = node + # if l_node.is_a?(YAML::Nodes::Scalar) + # value_patched = Crinja.render(l_node.value, {"env" => ENV.to_h}) + # return value_patched + # end + + # return "" + # end + # + # def self.to_yaml(value, builder : YAML::Nodes::Builder) + # end + # end + + class RootConfig + include YAML::Serializable + include YAML::Serializable::Strict + + # @[YAML::Field(key: "version", converter: GX::Models::CrinjaConverter)] + @[YAML::Field(key: "version")] + getter version : String + + @[YAML::Field(key: "global")] + getter global : GlobalConfig + + @[YAML::Field(key: "filesystems")] + getter filesystems : Array(AbstractFilesystemConfig) + end +end + diff --git a/src/models/sshfs_config.cr b/src/models/sshfs_config.cr new file mode 100644 index 0000000..6b7e35c --- /dev/null +++ b/src/models/sshfs_config.cr @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland +# Copyright © 2023 Glenn Y. Rolland + +require "shellwords" +require "./abstract_filesystem_config" +require "./concerns/base" + +module GX::Models + class SshFSConfig < AbstractFilesystemConfig + getter remote_path : String = "" + getter remote_user : String = "" + getter remote_host : String = "" + getter remote_port : String = "22" + + include Concerns::Base + + def _mounted_prefix() + "#{@remote_user}@#{@remote_host}:#{@remote_path}" + end + + def mounted_name() + @name + end + + def _mount_action() + mount_point_safe = @mount_point + raise "Invalid mount point" if mount_point_safe.nil? + + process = Process.new( + "sshfs", + [ + "-p", remote_port, + "#{@remote_user}@#{@remote_host}:#{@remote_path}", + mount_point_safe + ], + input: STDIN, + output: STDOUT, + error: STDERR + ) + unless process.wait.success? + puts "Error mounting the filesystem".colorize(:red) + return + end + end + end +end -- 2.45.2 From cb14a04fbedf7cbad3cc2731b0eb6c31b83a7421 Mon Sep 17 00:00:00 2001 From: Glenn Date: Fri, 24 Nov 2023 10:25:30 +0100 Subject: [PATCH 09/32] feat: add support for auto-open option (-o, --open) --- src/cli.cr | 56 ++++++++++++++++++++++-- src/config.cr | 14 +++--- src/models/abstract_filesystem_config.cr | 6 +++ src/models/concerns/base.cr | 4 +- src/models/global_config.cr | 6 ++- src/models/gocryptfs_config.cr | 2 +- src/models/httpdirfs_config.cr | 2 +- src/models/sshfs_config.cr | 2 +- 8 files changed, 77 insertions(+), 15 deletions(-) diff --git a/src/cli.cr b/src/cli.cr index 844fa91..327fc92 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -38,6 +38,11 @@ module GX @config.verbose = true end + parser.on("-o", "--open", "Automatically open directory after mount") do |flag| + Log.info { "Auto-open enabled" } + @config.auto_open = true + end + parser.on("--version", "Show version") do |flag| @config.mode = Config::Mode::ShowVersion end @@ -93,10 +98,56 @@ module GX when Config::Mode::Mount @config.load_from_file filesystem = choose_filesystem - mount_or_umount(filesystem) if !filesystem.nil? + raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + + mount_or_umount(filesystem) + auto_open(filesystem) if @config.auto_open end end + def auto_open(filesystem) + # FIXME: support xdg-open + # FIXME: support mailcap + # FIXME: support user-defined command + # FIXME: detect graphical environment + + mount_point_safe = filesystem.mount_point + raise Models::InvalidMountpointError.new("Invalid filesystem") if mount_point_safe.nil? + + if graphical_environment? + process = Process.new( + "xdg-open", ## FIXME: make configurable + [mount_point_safe], + input: STDIN, + output: STDOUT, + error: STDERR + ) + unless process.wait.success? + puts "Error opening filesystem".colorize(:red) + return + end + else + process = Process.new( + "vifm", ## FIXME: make configurable + [mount_point_safe], + input: STDIN, + output: STDOUT, + error: STDERR + ) + unless process.wait.success? + puts "Error opening filesystem".colorize(:red) + return + end + end + end + + def graphical_environment? + if ENV["DISPLAY"]? || ENV["WAYLAND_DISPLAY"]? + return true + end + return false + end + def choose_filesystem() names_display = {} of String => NamedTuple(filesystem: Models::AbstractFilesystemConfig, ansi_name: String) @@ -134,9 +185,6 @@ module GX end def mount_or_umount(selected_filesystem) - config_root_safe = @config.root - return if config_root_safe.nil? - if !selected_filesystem.mounted? selected_filesystem.mount() else diff --git a/src/config.cr b/src/config.cr index c936b77..9d81936 100644 --- a/src/config.cr +++ b/src/config.cr @@ -11,6 +11,9 @@ module GX class Config Log = ::Log.for("config") + class MissingFileError < Exception + end + enum Mode ConfigAdd ConfigDelete @@ -31,14 +34,15 @@ module GX property mode : Mode property path : String? property args : AddArgs.class | DelArgs.class | NoArgs.class + property auto_open : Bool def initialize() - if !ENV["HOME"]? - raise "Home directory not found" - end + raise Models::InvalidEnvironmentError.new("Home directory not found") if !ENV["HOME"]? @home_dir = ENV["HOME"] @verbose = false + @auto_open = false + @mode = Mode::Mount @filesystems = [] of Models::AbstractFilesystemConfig @path = nil @@ -66,7 +70,7 @@ module GX end Log.error { "No configuration file found in any of the standard locations" } - raise "Configuration file not found" + raise MissingFileError.new("Configuration file not found") end def load_from_file @@ -87,7 +91,7 @@ module GX root = Models::RootConfig.from_yaml(file_patched) global_mount_point = root.global.mount_point - raise "Invalid global mount point" if global_mount_point.nil? + raise Models::InvalidMountpointError.new("Invalid global mount point") if global_mount_point.nil? root.filesystems.each do |selected_filesystem| if !selected_filesystem.mount_point? diff --git a/src/models/abstract_filesystem_config.cr b/src/models/abstract_filesystem_config.cr index d1006d8..afa55bf 100644 --- a/src/models/abstract_filesystem_config.cr +++ b/src/models/abstract_filesystem_config.cr @@ -6,6 +6,12 @@ require "yaml" module GX::Models + class InvalidFilesystemError < Exception + end + + class InvalidMountpointError < Exception + end + abstract class AbstractFilesystemConfig include YAML::Serializable # include YAML::Serializable::Strict diff --git a/src/models/concerns/base.cr b/src/models/concerns/base.cr index 9f0d656..a5afda5 100644 --- a/src/models/concerns/base.cr +++ b/src/models/concerns/base.cr @@ -3,14 +3,14 @@ module GX::Models::Concerns module Base def mounted?() : Bool mount_point_safe = @mount_point - raise "Invalid mountpoint value" if mount_point_safe.nil? + raise InvalidMountpointError.new("Invalid mountpoint value") if mount_point_safe.nil? `mount`.includes?(" on #{mount_point_safe} type ") end def umount() : Nil mount_point_safe = @mount_point - raise "Invalid mountpoint value" if mount_point_safe.nil? + raise InvalidMountpointError.new("Invalid mountpoint value") if mount_point_safe.nil? system("fusermount -u #{mount_point_safe.shellescape}") fusermount_status = $? diff --git a/src/models/global_config.cr b/src/models/global_config.cr index 25759b5..91cf2b7 100644 --- a/src/models/global_config.cr +++ b/src/models/global_config.cr @@ -7,6 +7,9 @@ require "yaml" require "./abstract_filesystem_config" module GX::Models + class InvalidEnvironmentError < Exception + end + class GlobalConfig include YAML::Serializable include YAML::Serializable::Strict @@ -15,7 +18,8 @@ module GX::Models getter mount_point : String? def after_initialize() - home_dir = ENV["HOME"] || raise "Home directory not found" + raise InvalidEnvironmentError.new("Home directory not found") if !ENV["HOME"]? + home_dir = ENV["HOME"] # Set default mountpoint from global if none defined if @mount_point.nil? || @mount_point.try &.empty? diff --git a/src/models/gocryptfs_config.cr b/src/models/gocryptfs_config.cr index 2dd3cb8..5d08263 100644 --- a/src/models/gocryptfs_config.cr +++ b/src/models/gocryptfs_config.cr @@ -23,7 +23,7 @@ module GX::Models def _mount_action() mount_point_safe = @mount_point - raise "Invalid mount point" if mount_point_safe.nil? + raise InvalidMountpointError.new("Invalid mount point") if mount_point_safe.nil? process = Process.new( "gocryptfs", diff --git a/src/models/httpdirfs_config.cr b/src/models/httpdirfs_config.cr index 29ab5dd..c175558 100644 --- a/src/models/httpdirfs_config.cr +++ b/src/models/httpdirfs_config.cr @@ -23,7 +23,7 @@ module GX::Models def _mount_action() mount_point_safe = @mount_point - raise "Invalid mount point" if mount_point_safe.nil? + raise InvalidMountpointError.new("Invalid mount point") if mount_point_safe.nil? process = Process.new( "httpdirfs", diff --git a/src/models/sshfs_config.cr b/src/models/sshfs_config.cr index 6b7e35c..c7e5853 100644 --- a/src/models/sshfs_config.cr +++ b/src/models/sshfs_config.cr @@ -26,7 +26,7 @@ module GX::Models def _mount_action() mount_point_safe = @mount_point - raise "Invalid mount point" if mount_point_safe.nil? + raise InvalidMountpointError.new("Invalid mount point") if mount_point_safe.nil? process = Process.new( "sshfs", -- 2.45.2 From 5107e80aa7d790587ad20241358f54043c08ca07 Mon Sep 17 00:00:00 2001 From: glenux Date: Fri, 24 Nov 2023 09:29:12 +0000 Subject: [PATCH 10/32] Update README.md --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9fa994b..383e101 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,12 @@ version](https://code.apps.glenux.net/glenux/mfm/releases) of MFM. ``` Usage: mfm [options] -Global options: - -c, --config FILE Specify configuration file - -h, --help Display this help +Global options + -c, --config FILE Set configuration file + -v, --verbose Set more verbosity + -o, --open Automatically open directory after mount + --version Show version + -h, --help Show this help Commands (not implemented yet): create Add a new filesystem -- 2.45.2 From 211419ea029fa7e9931dc3c188456d99bd06a65b Mon Sep 17 00:00:00 2001 From: Glenn Date: Fri, 24 Nov 2023 10:48:55 +0100 Subject: [PATCH 11/32] chore: add watch+rebuild target --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5e6983c..013ad50 100644 --- a/Makefile +++ b/Makefile @@ -6,4 +6,8 @@ all: build build: - shards build + shards build --error-trace + @echo SUCCESS + +watch: + watchexec --restart --delay-run 3 -c -e cr make build -- 2.45.2 From 84230a6828023bc17a807df68a51f8eb5c807831 Mon Sep 17 00:00:00 2001 From: Glenn Date: Fri, 24 Nov 2023 10:49:32 +0100 Subject: [PATCH 12/32] feat: sort by fs.name instead of fs.type --- src/cli.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli.cr b/src/cli.cr index 327fc92..6763a1d 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -173,7 +173,9 @@ module GX } end - result_filesystem_name = Fzf.run(names_display.values.map(&.[:ansi_name]).sort).strip + ## FIXME: feat: allow to sort by name or by filesystem + sorted_values = names_display.values.sort_by { |item| item[:filesystem].name } + result_filesystem_name = Fzf.run(sorted_values.map(&.[:ansi_name])).strip selected_filesystem = names_display[result_filesystem_name][:filesystem] puts ">> #{selected_filesystem.name}".colorize(:yellow) -- 2.45.2 From 32fea233d12991c3c5a33a5438a2967d600ab107 Mon Sep 17 00:00:00 2001 From: Glenn Date: Fri, 24 Nov 2023 10:49:55 +0100 Subject: [PATCH 13/32] fix: rename global.mount_point to avoid misunderstanding --- src/config.cr | 6 +++--- src/models/global_config.cr | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/config.cr b/src/config.cr index 9d81936..17dbfd2 100644 --- a/src/config.cr +++ b/src/config.cr @@ -90,13 +90,13 @@ module GX root = Models::RootConfig.from_yaml(file_patched) - global_mount_point = root.global.mount_point - raise Models::InvalidMountpointError.new("Invalid global mount point") if global_mount_point.nil? + mount_point_base_safe = root.global.mount_point_base + raise Models::InvalidMountpointError.new("Invalid global mount point") if mount_point_base_safe.nil? root.filesystems.each do |selected_filesystem| if !selected_filesystem.mount_point? selected_filesystem.mount_point = - File.join(global_mount_point, selected_filesystem.mounted_name) + File.join(mount_point_base_safe, selected_filesystem.mounted_name) end end @root = root diff --git a/src/models/global_config.cr b/src/models/global_config.cr index 91cf2b7..2e827a7 100644 --- a/src/models/global_config.cr +++ b/src/models/global_config.cr @@ -14,16 +14,16 @@ module GX::Models include YAML::Serializable include YAML::Serializable::Strict - @[YAML::Field(key: "mount_point")] - getter mount_point : String? + @[YAML::Field(key: "mount_point_base")] + getter mount_point_base : String? def after_initialize() raise InvalidEnvironmentError.new("Home directory not found") if !ENV["HOME"]? home_dir = ENV["HOME"] # Set default mountpoint from global if none defined - if @mount_point.nil? || @mount_point.try &.empty? - @mount_point = File.join(home_dir, "mnt") + if @mount_point_base.nil? || @mount_point_base.try &.empty? + @mount_point_base = File.join(home_dir, "mnt") end end end -- 2.45.2 From 58e4ab05bfb7fdea95d4b28d743c7ad4c9c072ac Mon Sep 17 00:00:00 2001 From: glenux Date: Fri, 24 Nov 2023 09:52:05 +0000 Subject: [PATCH 14/32] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 383e101..c1abafe 100644 --- a/README.md +++ b/README.md @@ -87,10 +87,11 @@ detail the filesystem names, types, and respective configurations. ### YAML File Format ```yaml -# NOT IMPLEMENTED YET -# version: "1" -# global: -# mountpoint: "/home/user/mnt/{{filesystem.name}}" +--- +version: "1" + +global: + mountpoint: "{{env.HOME}}/mnt" filesystems: - type: "gocryptfs" -- 2.45.2 From 70b51527dfaa714b3e746da4b194b6b47a005f84 Mon Sep 17 00:00:00 2001 From: Glenn Date: Fri, 24 Nov 2023 17:02:17 +0100 Subject: [PATCH 15/32] doc: update example config to demonstrate templating --- doc/demo.mfm.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/demo.mfm.yml b/doc/demo.mfm.yml index 169fde8..8c8ff2b 100644 --- a/doc/demo.mfm.yml +++ b/doc/demo.mfm.yml @@ -1,9 +1,8 @@ --- - version: 1 global: - mountpoint: "~/mnt" + mountpoint: "{{env.HOME}}/mnt" filesystems: - type: gocryptfs @@ -16,7 +15,7 @@ filesystems: - type: sshfs name: "Personal - Remote Media Server" - remote_user: user + remote_user: "{{env.USER}}" remote_host: mediaserver.local remote_port: 22 remote_path: "/remote/path/to/media" -- 2.45.2 From 041550cc0f3d864209fe03db386517632ecf72eb Mon Sep 17 00:00:00 2001 From: Glenn Date: Fri, 24 Nov 2023 19:25:21 +0100 Subject: [PATCH 16/32] fix: handle mount errors (with the right message) --- src/models/concerns/base.cr | 9 +++++++-- src/models/gocryptfs_config.cr | 5 +---- src/models/httpdirfs_config.cr | 5 +---- src/models/sshfs_config.cr | 5 +---- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/models/concerns/base.cr b/src/models/concerns/base.cr index a5afda5..a12fab2 100644 --- a/src/models/concerns/base.cr +++ b/src/models/concerns/base.cr @@ -42,9 +42,14 @@ module GX::Models::Concerns return end - yield + result_status = yield - puts "Models #{name} is now available on #{mount_point_safe}".colorize(:green) + if result_status.success? + puts "Models #{name} is now available on #{mount_point_safe}".colorize(:green) + else + puts "Error mounting the vault".colorize(:red) + return + end end end diff --git a/src/models/gocryptfs_config.cr b/src/models/gocryptfs_config.cr index 5d08263..3cbcd61 100644 --- a/src/models/gocryptfs_config.cr +++ b/src/models/gocryptfs_config.cr @@ -32,10 +32,7 @@ module GX::Models output: STDOUT, error: STDERR ) - unless process.wait.success? - puts "Error mounting the vault".colorize(:red) - return - end + return process.wait end end end diff --git a/src/models/httpdirfs_config.cr b/src/models/httpdirfs_config.cr index c175558..fdcf012 100644 --- a/src/models/httpdirfs_config.cr +++ b/src/models/httpdirfs_config.cr @@ -32,10 +32,7 @@ module GX::Models output: STDOUT, error: STDERR ) - unless process.wait.success? - puts "Error mounting the filesystem".colorize(:red) - return - end + return process.wait end end end diff --git a/src/models/sshfs_config.cr b/src/models/sshfs_config.cr index c7e5853..fee42e5 100644 --- a/src/models/sshfs_config.cr +++ b/src/models/sshfs_config.cr @@ -39,10 +39,7 @@ module GX::Models output: STDOUT, error: STDERR ) - unless process.wait.success? - puts "Error mounting the filesystem".colorize(:red) - return - end + return process.wait end end end -- 2.45.2 From 9f3f3b24c18c7f5f855cf10a76098447207d6f71 Mon Sep 17 00:00:00 2001 From: Glenn Date: Fri, 24 Nov 2023 19:25:50 +0100 Subject: [PATCH 17/32] fix: auto-open should not run on umounted filesystems --- src/cli.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.cr b/src/cli.cr index 6763a1d..b15e8be 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -101,7 +101,7 @@ module GX raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? mount_or_umount(filesystem) - auto_open(filesystem) if @config.auto_open + auto_open(filesystem) if filesystem.mounted? && @config.auto_open end end -- 2.45.2 From 3a30fd8a866fefc6feb7d7c51a6e93809657afa7 Mon Sep 17 00:00:00 2001 From: Glenn Date: Sun, 26 Nov 2023 16:21:47 +0100 Subject: [PATCH 18/32] feat: add defaut FZF options when none defined --- src/cli.cr | 1 + src/config.cr | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/cli.cr b/src/cli.cr index b15e8be..631d390 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -96,6 +96,7 @@ module GX when Config::Mode::ShowVersion STDOUT.puts "#{PROGRAM_NAME} #{VERSION}" when Config::Mode::Mount + @config.load_from_env @config.load_from_file filesystem = choose_filesystem raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? diff --git a/src/config.cr b/src/config.cr index 17dbfd2..bb0d0d7 100644 --- a/src/config.cr +++ b/src/config.cr @@ -73,6 +73,13 @@ module GX raise MissingFileError.new("Configuration file not found") end + def load_from_env() + if !ENV["FZF_DEFAULT_OPTS"]? + # force defaults settings if none defined + ENV["FZF_DEFAULT_OPTS"] = "--height 40% --layout=reverse --border" + end + end + def load_from_file config_path = @path if config_path.nil? -- 2.45.2 From dd5aa1db6f3814cf4bea17d94bc2a657b53dd1ac Mon Sep 17 00:00:00 2001 From: Glenn Date: Wed, 3 Jan 2024 19:32:17 +0100 Subject: [PATCH 19/32] doc: add preamble to README --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index c1abafe..cd7bfd8 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,13 @@ --> [![Build Status](https://cicd.apps.glenux.net/api/badges/glenux/mfm/status.svg)](https://cicd.apps.glenux.net/glenux/mfm) +![License LGPL3.0-or-later](https://img.shields.io/badge/license-LGPL3.0--or--later-blue.svg) +[![Donate on patreon](https://img.shields.io/badge/patreon-donate-orange.svg)](https://patreon.com/glenux) + +> :information_source: This project is available on our self-hosted server and +> on CodeBerg and GitHub as mirrors. For the latest updates and comprehensive +> version of our project, please visit our primary repository at: +> . # Minimalist Fuse Manager (MFM) -- 2.45.2 From 35a87cd7e031e9ed5f4ff0fe109e6e91d6de0d7c Mon Sep 17 00:00:00 2001 From: Glenn Date: Sun, 7 Jan 2024 17:47:11 +0100 Subject: [PATCH 20/32] feat: add basic support for bash completion --- static/completion.bash | 45 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 static/completion.bash diff --git a/static/completion.bash b/static/completion.bash new file mode 100644 index 0000000..d990879 --- /dev/null +++ b/static/completion.bash @@ -0,0 +1,45 @@ +#!/bin/bash + +# mfm Bash completion script + +_mfm() { + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + # Options globales pour 'mfm' + local mfm_global_opts="-c --config -v --verbose -o --open --version -h --help" + + # Commandes pour 'mfm' + local mfm_cmds="config mapping completion" + + # Options pour 'config' et 'mapping' + local config_opts="init" + local mapping_opts="list create edit mount umount delete" + + # Ajouter les options globales à chaque cas + case "${prev}" in + mfm) + COMPREPLY=($(compgen -W "${mfm_cmds} ${mfm_global_opts}" -- ${cur})) + return 0 + ;; + config) + COMPREPLY=($(compgen -W "${config_opts} ${mfm_global_opts}" -- ${cur})) + return 0 + ;; + mapping) + COMPREPLY=($(compgen -W "${mapping_opts} ${mfm_global_opts}" -- ${cur})) + return 0 + ;; + *) + if [[ ${cur} == -* ]]; then + COMPREPLY=($(compgen -W "${mfm_global_opts}" -- ${cur})) + fi + return 0 + ;; + esac +} + +# Appliquer la complétion à la fonction 'mfm' +complete -F _mfm mfm -- 2.45.2 From f279879ce06152caf0e5e5d013c13baf3c94c76f Mon Sep 17 00:00:00 2001 From: Glenn Date: Sun, 7 Jan 2024 19:45:39 +0100 Subject: [PATCH 21/32] chore: add test & install to Makefile --- Makefile | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Makefile b/Makefile index 013ad50..3894aa9 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,29 @@ # SPDX-FileCopyrightText: 2023 Glenn Y. Rolland # Copyright © 2023 Glenn Y. Rolland +PREFIX=/usr + all: build +prepare: + shards install + build: shards build --error-trace @echo SUCCESS watch: watchexec --restart --delay-run 3 -c -e cr make build + +spec: test +test: + crystal spec --error-trace + +install: + install \ + -m 755 \ + bin/code-preloader \ + $(PREFIX)/bin + +.PHONY: spec test build all prepare install + -- 2.45.2 From 642be926845739f34e201d0a5a2b42a605bad692 Mon Sep 17 00:00:00 2001 From: Glenn Date: Sun, 7 Jan 2024 19:46:09 +0100 Subject: [PATCH 22/32] chore: add code-preloader config file --- .code_preloader.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .code_preloader.yml diff --git a/.code_preloader.yml b/.code_preloader.yml new file mode 100644 index 0000000..5b28953 --- /dev/null +++ b/.code_preloader.yml @@ -0,0 +1,25 @@ +--- +# Example configuration for Code-Preloader + +# List of repository paths to preload +# source_list: +# - "path/to/repo1" +# - "path/to/repo2" + +# List of patterns to ignore during preloading +ignore_list: + - ^\.git/.* + - ^lib.* + +# Path to the output file (if null, output to STDOUT) +output_path: null + +prompt: + # Optional: Path to a file containing the prompt header + header_path: null + + # Optional: Path to a file containing the prompt footer + footer_path: null + + # Optional: Path to a file container a jinja template to structure the prompt + template_path: null -- 2.45.2 From cbf39027c5ccc74d58e73d24c836fcca859a176a Mon Sep 17 00:00:00 2001 From: Glenn Date: Tue, 9 Jan 2024 22:32:22 +0100 Subject: [PATCH 23/32] ci: bump crystal version --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index 8999d06..1599bbe 100644 --- a/.drone.yml +++ b/.drone.yml @@ -5,7 +5,7 @@ name: default steps: - name: build:binary - image: crystallang/crystal:1.10.1-alpine + image: crystallang/crystal:1.11.0-alpine environment: PACKAGE_BASENAME: mfm_linux_amd64 volumes: -- 2.45.2 From 9ef261779ce0ec5332fe34b5689a78835e0a738d Mon Sep 17 00:00:00 2001 From: Glenn Date: Sun, 14 Jan 2024 20:31:38 +0100 Subject: [PATCH 24/32] refactor: introduce command design pattern --- src/cli.cr | 184 ++------------------- src/command_factory.cr | 11 ++ src/commands.cr | 1 + src/commands/abstract_command.cr | 13 ++ src/commands/config_init.cr | 15 ++ src/commands/global_completion.cr | 15 ++ src/commands/global_config.cr | 15 ++ src/commands/global_help.cr | 18 ++ src/commands/global_mapping.cr | 16 ++ src/commands/global_tui.cr | 25 +++ src/commands/global_version.cr | 17 ++ src/commands/mapping_create.cr | 16 ++ src/commands/mapping_delete.cr | 16 ++ src/commands/mapping_edit.cr | 16 ++ src/commands/mapping_list.cr | 45 +++++ src/commands/mapping_mount.cr | 26 +++ src/commands/mapping_umount.cr | 24 +++ src/config.cr | 34 ++-- src/file_system_manager.cr | 142 ++++++++++++++++ src/main.cr | 2 - src/models/abstract_filesystem_config.cr | 20 +-- src/models/concerns/base.cr | 10 +- src/models/global_config.cr | 4 +- src/models/gocryptfs_config.cr | 14 +- src/models/httpdirfs_config.cr | 14 +- src/models/root_config.cr | 7 +- src/models/sshfs_config.cr | 18 +- src/parsers/base.cr | 5 + src/parsers/completion_parser.cr | 26 +++ src/parsers/config_parser.cr | 38 +++++ src/parsers/mapping_parser.cr | 73 ++++++++ src/parsers/options/config_init_options.cr | 7 + src/parsers/options/config_options.cr | 6 + src/parsers/options/help_options.cr | 7 + src/parsers/root_parser.cr | 92 +++++++++++ src/types/modes.cr | 21 +++ src/utils/breadcrumbs.cr | 19 +++ src/{ => utils}/fzf.cr | 6 +- src/utils/parser_lines.cr | 17 ++ src/version.cr | 3 - 40 files changed, 817 insertions(+), 241 deletions(-) create mode 100644 src/command_factory.cr create mode 100644 src/commands.cr create mode 100644 src/commands/abstract_command.cr create mode 100644 src/commands/config_init.cr create mode 100644 src/commands/global_completion.cr create mode 100644 src/commands/global_config.cr create mode 100644 src/commands/global_help.cr create mode 100644 src/commands/global_mapping.cr create mode 100644 src/commands/global_tui.cr create mode 100644 src/commands/global_version.cr create mode 100644 src/commands/mapping_create.cr create mode 100644 src/commands/mapping_delete.cr create mode 100644 src/commands/mapping_edit.cr create mode 100644 src/commands/mapping_list.cr create mode 100644 src/commands/mapping_mount.cr create mode 100644 src/commands/mapping_umount.cr create mode 100644 src/file_system_manager.cr create mode 100644 src/parsers/base.cr create mode 100644 src/parsers/completion_parser.cr create mode 100644 src/parsers/config_parser.cr create mode 100644 src/parsers/mapping_parser.cr create mode 100644 src/parsers/options/config_init_options.cr create mode 100644 src/parsers/options/config_options.cr create mode 100644 src/parsers/options/help_options.cr create mode 100644 src/parsers/root_parser.cr create mode 100644 src/types/modes.cr create mode 100644 src/utils/breadcrumbs.cr rename src/{ => utils}/fzf.cr (92%) create mode 100644 src/utils/parser_lines.cr diff --git a/src/cli.cr b/src/cli.cr index 631d390..4366d07 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -5,194 +5,38 @@ require "option_parser" require "./config" -require "./fzf" require "./version" +require "./parsers/root_parser" +require "./utils/breadcrumbs" +require "./utils/fzf" +require "./file_system_manager" +require "./command_factory" module GX class Cli Log = ::Log.for("cli") - @config : Config + @config : GX::Config - def initialize() + def initialize # Main execution starts here + # # FIXME: add a method to verify that FZF is installed @config = Config.new - - ## FIXME: check that FZF is installed end def parse_command_line(args) - # update - add_args = { name: "", path: "" } - delete_args = { name: "" } pparser = OptionParser.new do |parser| - parser.banner = "Usage: #{PROGRAM_NAME} [options]\n\nGlobal options" - - parser.on("-c", "--config FILE", "Set configuration file") do |path| - Log.info { "Configuration set to #{path}" } - @config.path = path - end - - parser.on("-v", "--verbose", "Set more verbosity") do |flag| - Log.info { "Verbosity enabled" } - @config.verbose = true - end - - parser.on("-o", "--open", "Automatically open directory after mount") do |flag| - Log.info { "Auto-open enabled" } - @config.auto_open = true - end - - parser.on("--version", "Show version") do |flag| - @config.mode = Config::Mode::ShowVersion - end - - parser.on("-h", "--help", "Show this help") do |flag| - STDOUT.puts parser - exit(0) - end - - parser.separator("\nCommands") - parser.on("config", "Manage configuration") do - parser.banner = "Usage: #{PROGRAM_NAME} config [commands] [options]\n\nGlobal options" - parser.separator("\nCommands") - - parser.on("create", "Create vault") do - @config.mode = Config::Mode::ConfigAdd - - parser.banner = "Usage: #{PROGRAM_NAME} config create [commands] [options]\n\nGlobal options" - parser.separator("\nCommand options") - - parser.on("-n", "--name", "Set vault name") do |name| - add_args = add_args.merge({ name: name }) - end - parser.on("-p", "--path", "Set vault encrypted path") do |path| - add_args = add_args.merge({ path: path }) - end - end - - parser.on("delete", "Delete vault") do - @config.mode = Config::Mode::ConfigAdd - - parser.banner = "Usage: #{PROGRAM_NAME} delete [options]\n\nGlobal options" - parser.separator("\nCommand options") - - parser.on("-n", "--name", "Set vault name") do |name| - delete_args = delete_args.merge({ name: name }) - end - end - - parser.on("edit", "Edit configuration") do |flag| - @config.mode = Config::Mode::ConfigEdit - end - end - + breadcrumbs = Utils::BreadCrumbs.new([] of String) + Parsers::RootParser.new.build(parser, breadcrumbs, @config) end pparser.parse(args) end - def run() - case @config.mode - when Config::Mode::ShowVersion - STDOUT.puts "#{PROGRAM_NAME} #{VERSION}" - when Config::Mode::Mount - @config.load_from_env - @config.load_from_file - filesystem = choose_filesystem - raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + def run + command = CommandFactory.create_command(@config, @config.mode) + abort("ERROR: unknown command for mode #{@config.mode}") if command.nil? - mount_or_umount(filesystem) - auto_open(filesystem) if filesystem.mounted? && @config.auto_open - end - end - - def auto_open(filesystem) - # FIXME: support xdg-open - # FIXME: support mailcap - # FIXME: support user-defined command - # FIXME: detect graphical environment - - mount_point_safe = filesystem.mount_point - raise Models::InvalidMountpointError.new("Invalid filesystem") if mount_point_safe.nil? - - if graphical_environment? - process = Process.new( - "xdg-open", ## FIXME: make configurable - [mount_point_safe], - input: STDIN, - output: STDOUT, - error: STDERR - ) - unless process.wait.success? - puts "Error opening filesystem".colorize(:red) - return - end - else - process = Process.new( - "vifm", ## FIXME: make configurable - [mount_point_safe], - input: STDIN, - output: STDOUT, - error: STDERR - ) - unless process.wait.success? - puts "Error opening filesystem".colorize(:red) - return - end - end - end - - def graphical_environment? - if ENV["DISPLAY"]? || ENV["WAYLAND_DISPLAY"]? - return true - end - return false - end - - def choose_filesystem() - names_display = {} of String => NamedTuple(filesystem: Models::AbstractFilesystemConfig, ansi_name: String) - - config_root = @config.root - return if config_root.nil? - - config_root.filesystems.each do |filesystem| - fs_str = filesystem.type.ljust(12,' ') - - suffix = "" - suffix_ansi = "" - if filesystem.mounted? - suffix = "[open]" - suffix_ansi = "[#{ "open".colorize(:green) }]" - end - - result_name = "#{fs_str} #{filesystem.name} #{suffix}".strip - ansi_name = "#{fs_str.colorize(:dark_gray)} #{filesystem.name} #{suffix_ansi}".strip - - names_display[result_name] = { - filesystem: filesystem, - ansi_name: ansi_name - } - end - - ## FIXME: feat: allow to sort by name or by filesystem - sorted_values = names_display.values.sort_by { |item| item[:filesystem].name } - result_filesystem_name = 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) - return - end - return selected_filesystem - end - - def mount_or_umount(selected_filesystem) - if !selected_filesystem.mounted? - selected_filesystem.mount() - else - selected_filesystem.umount() - end + command.try &.execute end end end diff --git a/src/command_factory.cr b/src/command_factory.cr new file mode 100644 index 0000000..c2d7e22 --- /dev/null +++ b/src/command_factory.cr @@ -0,0 +1,11 @@ +require "./commands" + +module GX + class CommandFactory + def self.create_command(config : GX::Config, mode : GX::Types::Mode) : Commands::AbstractCommand? + classes = {{ Commands::AbstractCommand.all_subclasses }} + command_klass = classes.find { |klass| klass.handles_mode == mode } + command_klass.try &.new(config) + end + end +end diff --git a/src/commands.cr b/src/commands.cr new file mode 100644 index 0000000..034a066 --- /dev/null +++ b/src/commands.cr @@ -0,0 +1 @@ +require "./commands/*" diff --git a/src/commands/abstract_command.cr b/src/commands/abstract_command.cr new file mode 100644 index 0000000..7591f15 --- /dev/null +++ b/src/commands/abstract_command.cr @@ -0,0 +1,13 @@ +require "../config" + +module GX::Commands + abstract class AbstractCommand + abstract def initialize(config : GX::Config) + + abstract def execute + + def self.mode + Gx::Types::Mode::None + end + end +end diff --git a/src/commands/config_init.cr b/src/commands/config_init.cr new file mode 100644 index 0000000..b9407d3 --- /dev/null +++ b/src/commands/config_init.cr @@ -0,0 +1,15 @@ +require "./abstract_command" + +module GX::Commands + class ConfigInit < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + end + + def self.handles_mode + GX::Types::Mode::ConfigInit + end + end +end diff --git a/src/commands/global_completion.cr b/src/commands/global_completion.cr new file mode 100644 index 0000000..d1be696 --- /dev/null +++ b/src/commands/global_completion.cr @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000..c369556 --- /dev/null +++ b/src/commands/global_config.cr @@ -0,0 +1,15 @@ +require "./abstract_command" + +module GX::Commands + class GlobalConfig < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + end + + def self.handles_mode + GX::Types::Mode::GlobalConfig + end + end +end diff --git a/src/commands/global_help.cr b/src/commands/global_help.cr new file mode 100644 index 0000000..78f1616 --- /dev/null +++ b/src/commands/global_help.cr @@ -0,0 +1,18 @@ +require "./abstract_command" + +module GX::Commands + class GlobalHelp < AbstractCommand + def initialize(@config : GX::Config) # FIXME + end + + def execute + STDOUT.puts "" + @config.help_options.try { |opts| puts opts.parser_snapshot } + exit(0) + end + + def self.handles_mode + GX::Types::Mode::GlobalHelp + end + end +end diff --git a/src/commands/global_mapping.cr b/src/commands/global_mapping.cr new file mode 100644 index 0000000..0c61b95 --- /dev/null +++ b/src/commands/global_mapping.cr @@ -0,0 +1,16 @@ +require "./abstract_command" + +module GX::Commands + class GlobalMapping < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + # FIXME: implement + end + + def self.handles_mode + GX::Types::Mode::GlobalMapping + end + end +end diff --git a/src/commands/global_tui.cr b/src/commands/global_tui.cr new file mode 100644 index 0000000..58f22ed --- /dev/null +++ b/src/commands/global_tui.cr @@ -0,0 +1,25 @@ +require "./abstract_command" +require "../file_system_manager" + +module GX::Commands + class GlobalTui < AbstractCommand + @file_system_manager : FileSystemManager + + def initialize(@config : GX::Config) + @config.load_from_env + @config.load_from_file + @file_system_manager = FileSystemManager.new(@config) + end + + def execute + filesystem = @file_system_manager.choose_filesystem + raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + @file_system_manager.mount_or_umount(filesystem) + @file_system_manager.auto_open(filesystem) if filesystem.mounted? && @config.auto_open + end + + def self.handles_mode + GX::Types::Mode::GlobalTui + end + end +end diff --git a/src/commands/global_version.cr b/src/commands/global_version.cr new file mode 100644 index 0000000..9cd6757 --- /dev/null +++ b/src/commands/global_version.cr @@ -0,0 +1,17 @@ +require "./abstract_command" +require "../config" + +module GX::Commands + class GlobalVersion < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + STDOUT.puts "#{File.basename PROGRAM_NAME} #{VERSION}" + end + + def self.handles_mode + GX::Types::Mode::GlobalVersion + end + end +end diff --git a/src/commands/mapping_create.cr b/src/commands/mapping_create.cr new file mode 100644 index 0000000..1e3ec6f --- /dev/null +++ b/src/commands/mapping_create.cr @@ -0,0 +1,16 @@ +require "./abstract_command" + +module GX::Commands + class MappingCreate < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + # FIXME: implement + end + + def self.handles_mode + GX::Types::Mode::MappingCreate + end + end +end diff --git a/src/commands/mapping_delete.cr b/src/commands/mapping_delete.cr new file mode 100644 index 0000000..bed0534 --- /dev/null +++ b/src/commands/mapping_delete.cr @@ -0,0 +1,16 @@ +require "./abstract_command" + +module GX::Commands + class MappingDelete < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + # FIXME: implement + end + + def self.handles_mode + GX::Types::Mode::MappingDelete + end + end +end diff --git a/src/commands/mapping_edit.cr b/src/commands/mapping_edit.cr new file mode 100644 index 0000000..b0ef961 --- /dev/null +++ b/src/commands/mapping_edit.cr @@ -0,0 +1,16 @@ +require "./abstract_command" + +module GX::Commands + class MappingEdit < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + # FIXME: implement + end + + def self.handles_mode + GX::Types::Mode::MappingEdit + end + end +end diff --git a/src/commands/mapping_list.cr b/src/commands/mapping_list.cr new file mode 100644 index 0000000..35cd299 --- /dev/null +++ b/src/commands/mapping_list.cr @@ -0,0 +1,45 @@ +require "./abstract_command" +require "../file_system_manager" +require "tablo" + +module GX::Commands + class MappingList < AbstractCommand + def initialize(@config : GX::Config) + @config.load_from_env + @config.load_from_file + @file_system_manager = FileSystemManager.new(@config) + end + + def execute + filesystems = @config.root.try &.filesystems + return if filesystems.nil? + # pp filesystems + + fsdata = [] of Array(String) + filesystems.each do |item| + fsdata << [ + item.type, + item.name, + item.mounted?.to_s, + ] + end + # pp fsdata + + report = Tablo::Table.new( + fsdata, + # connectors: Tablo::CONNECTORS_SINGLE_ROUNDED + column_padding: 0, + style: "" # Tablo::STYLE_NO_MID_COL +) do |table| + table.add_column("TYPE") { |row| row[0] } + table.add_column("NAME", width: 40) { |row| row[1] } + table.add_column("MOUNTED") { |row| row[2] } + end + puts report + end + + def self.handles_mode + GX::Types::Mode::MappingList + end + end +end diff --git a/src/commands/mapping_mount.cr b/src/commands/mapping_mount.cr new file mode 100644 index 0000000..db81351 --- /dev/null +++ b/src/commands/mapping_mount.cr @@ -0,0 +1,26 @@ +require "./abstract_command" +require "../file_system_manager" + +module GX::Commands + class MappingMount < AbstractCommand + @file_system_manager : FileSystemManager + + def initialize(@config : GX::Config) # FIXME + @config.load_from_env + @config.load_from_file + @file_system_manager = FileSystemManager.new(@config) + end + + def execute + filesystem = @file_system_manager.choose_filesystem + raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + # @file_system_manager.mount_or_umount(filesystem) + filesystem.mount + @file_system_manager.auto_open(filesystem) if filesystem.mounted? && @config.auto_open + end + + def self.handles_mode + GX::Types::Mode::MappingMount + end + end +end diff --git a/src/commands/mapping_umount.cr b/src/commands/mapping_umount.cr new file mode 100644 index 0000000..dc61f1d --- /dev/null +++ b/src/commands/mapping_umount.cr @@ -0,0 +1,24 @@ +require "./abstract_command" +require "../file_system_manager" + +module GX::Commands + class MappingUmount < AbstractCommand + @file_system_manager : FileSystemManager + + def initialize(@config : GX::Config) # FIXME + @config.load_from_env + @config.load_from_file + @file_system_manager = FileSystemManager.new(@config) + end + + def execute + filesystem = @file_system_manager.choose_filesystem + raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + filesystem.umount + end + + def self.handles_mode + GX::Types::Mode::MappingUmount + end + end +end diff --git a/src/config.cr b/src/config.cr index bb0d0d7..d375cce 100644 --- a/src/config.cr +++ b/src/config.cr @@ -6,6 +6,11 @@ require "crinja" require "./models" +require "./types/modes" +require "./parsers/options/help_options" +require "./parsers/options/config_options" +require "./parsers/options/config_init_options" +require "./commands/abstract_command" module GX class Config @@ -14,14 +19,6 @@ module GX class MissingFileError < Exception end - enum Mode - ConfigAdd - ConfigDelete - ConfigEdit - ShowVersion - Mount - end - record NoArgs record AddArgs, name : String, path : String record DelArgs, name : String @@ -31,26 +28,31 @@ module GX getter root : Models::RootConfig? property verbose : Bool - property mode : Mode + property mode : Types::Mode property path : String? property args : AddArgs.class | DelArgs.class | NoArgs.class property auto_open : Bool - def initialize() + # FIXME: refactor and remove these parts from here + property help_options : Parsers::Options::HelpOptions? + property config_init_options : Parsers::Options::ConfigInitOptions? + property config_options : Parsers::Options::ConfigOptions? + + def initialize raise Models::InvalidEnvironmentError.new("Home directory not found") if !ENV["HOME"]? @home_dir = ENV["HOME"] @verbose = false @auto_open = false - @mode = Mode::Mount + @mode = Types::Mode::GlobalTui @filesystems = [] of Models::AbstractFilesystemConfig @path = nil @args = NoArgs end - private def detect_config_file() + private def detect_config_file possible_files = [ File.join(@home_dir, ".config", "mfm", "config.yaml"), File.join(@home_dir, ".config", "mfm", "config.yml"), @@ -73,8 +75,8 @@ module GX raise MissingFileError.new("Configuration file not found") end - def load_from_env() - if !ENV["FZF_DEFAULT_OPTS"]? + def load_from_env + if !ENV["FZF_DEFAULT_OPTS"]? # force defaults settings if none defined ENV["FZF_DEFAULT_OPTS"] = "--height 40% --layout=reverse --border" end @@ -93,7 +95,7 @@ module GX end file_data = File.read(config_path) - file_patched = Crinja.render(file_data, {"env" => ENV.to_h}) + file_patched = Crinja.render(file_data, {"env" => ENV.to_h}) root = Models::RootConfig.from_yaml(file_patched) @@ -102,7 +104,7 @@ module GX root.filesystems.each do |selected_filesystem| if !selected_filesystem.mount_point? - selected_filesystem.mount_point = + selected_filesystem.mount_point = File.join(mount_point_base_safe, selected_filesystem.mounted_name) end end diff --git a/src/file_system_manager.cr b/src/file_system_manager.cr new file mode 100644 index 0000000..91dc1a8 --- /dev/null +++ b/src/file_system_manager.cr @@ -0,0 +1,142 @@ +# require "./models/abstract_filesystem_config" +require "./utils/fzf" + +module GX + class FileSystemManager + Log = ::Log.for("file_system_manager") + + def initialize(@config : Config) + end + + # OBSOLETE: + # def mount_filesystem(filesystem : Models::AbstractFilesystemConfig) + # raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + # if filesystem.mounted? + # Log.info { "Filesystem already mounted." } + # return + # end + # filesystem.mount + # end + + # OBSOLETE: + # def umount_filesystem(filesystem : Models::AbstractFilesystemConfig) + # raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + # unless filesystem.mounted? + # Log.info { "Filesystem is not mounted." } + # return + # end + # filesystem.umount + # end + + def mount_or_umount(selected_filesystem) + if !selected_filesystem.mounted? + selected_filesystem.mount + else + selected_filesystem.umount + end + end + + def auto_open(filesystem) + # FIXME: detect xdg-open and use it if possible + # FIXME: detect mailcap and use it if no xdg-open found + # FIXME: support user-defined command in configuration + # FIXME: detect graphical environment + + mount_point_safe = filesystem.mount_point + raise Models::InvalidMountpointError.new("Invalid filesystem") if mount_point_safe.nil? + + if graphical_environment? + process = Process.new( + "xdg-open", # # FIXME: make configurable + [mount_point_safe], + input: STDIN, + output: STDOUT, + error: STDERR + ) + unless process.wait.success? + puts "Error opening filesystem".colorize(:red) + return + end + else + process = Process.new( + "vifm", # # FIXME: make configurable + [mount_point_safe], + input: STDIN, + output: STDOUT, + error: STDERR + ) + unless process.wait.success? + puts "Error opening filesystem".colorize(:red) + return + end + end + end + + def each(&) + config_root = @config.root + return if config_root.nil? + + config_root.filesystems.each do |filesystem| + yield filesystem + end + end + + def filesystems + config_root = @config.root + return if config_root.nil? + + config_root.filesystems + end + + def choose_filesystem + names_display = {} of String => NamedTuple(filesystem: Models::AbstractFilesystemConfig, ansi_name: String) + + config_root = @config.root + return if config_root.nil? + + config_root.filesystems.each do |filesystem| + fs_str = filesystem.type.ljust(12, ' ') + + suffix = "" + suffix_ansi = "" + if filesystem.mounted? + suffix = "[open]" + suffix_ansi = "[#{"open".colorize(:green)}]" + end + + result_name = "#{fs_str} #{filesystem.name} #{suffix}".strip + ansi_name = "#{fs_str.colorize(:dark_gray)} #{filesystem.name} #{suffix_ansi}".strip + + names_display[result_name] = { + filesystem: filesystem, + ansi_name: ansi_name, + } + end + + # # FIXME: feat: allow to sort by name or by filesystem + sorted_values = names_display.values.sort_by { |item| item[: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) + return + end + return selected_filesystem + end + + private def generate_display_name(filesystem : Models::AbstractFilesystemConfig) : String + fs_str = filesystem.type.ljust(12, ' ') + suffix = filesystem.mounted? ? "[open]" : "" + "#{fs_str} #{filesystem.name} #{suffix}".strip + end + + private def graphical_environment? + if ENV["DISPLAY"]? || ENV["WAYLAND_DISPLAY"]? + return true + end + return false + end + end +end diff --git a/src/main.cr b/src/main.cr index 2fc1327..28e57a4 100644 --- a/src/main.cr +++ b/src/main.cr @@ -34,5 +34,3 @@ end app = GX::Cli.new app.parse_command_line(ARGV) app.run - - diff --git a/src/models/abstract_filesystem_config.cr b/src/models/abstract_filesystem_config.cr index afa55bf..928a0d7 100644 --- a/src/models/abstract_filesystem_config.cr +++ b/src/models/abstract_filesystem_config.cr @@ -17,9 +17,9 @@ module GX::Models # include YAML::Serializable::Strict use_yaml_discriminator "type", { - gocryptfs: GoCryptFSConfig, - sshfs: SshFSConfig, - httpdirfs: HttpDirFSConfig + gocryptfs: GoCryptFSConfig, + sshfs: SshFSConfig, + httpdirfs: HttpDirFSConfig, } getter type : String @@ -27,12 +27,12 @@ module GX::Models property mount_point : String? abstract def _mount_wrapper(&block) - abstract def _mount_action() - abstract def _mounted_prefix() - abstract def mounted_name() - abstract def mounted?() - abstract def mount() - abstract def umount() - abstract def mount_point?() + abstract def _mount_action + abstract def _mounted_prefix + abstract def mounted_name + abstract def mounted? + abstract def mount + abstract def umount + abstract def mount_point? end end diff --git a/src/models/concerns/base.cr b/src/models/concerns/base.cr index a12fab2..90254de 100644 --- a/src/models/concerns/base.cr +++ b/src/models/concerns/base.cr @@ -1,14 +1,13 @@ - module GX::Models::Concerns module Base - def mounted?() : Bool + def mounted? : Bool mount_point_safe = @mount_point raise InvalidMountpointError.new("Invalid mountpoint value") if mount_point_safe.nil? `mount`.includes?(" on #{mount_point_safe} type ") end - def umount() : Nil + def umount : Nil mount_point_safe = @mount_point raise InvalidMountpointError.new("Invalid mountpoint value") if mount_point_safe.nil? @@ -22,11 +21,11 @@ module GX::Models::Concerns end end - def mount_point?() + def mount_point? !mount_point.nil? end - def mount() + def mount _mount_wrapper() do _mount_action end @@ -52,5 +51,4 @@ module GX::Models::Concerns end end end - end diff --git a/src/models/global_config.cr b/src/models/global_config.cr index 2e827a7..2cd57c1 100644 --- a/src/models/global_config.cr +++ b/src/models/global_config.cr @@ -17,7 +17,7 @@ module GX::Models @[YAML::Field(key: "mount_point_base")] getter mount_point_base : String? - def after_initialize() + def after_initialize raise InvalidEnvironmentError.new("Home directory not found") if !ENV["HOME"]? home_dir = ENV["HOME"] @@ -28,5 +28,3 @@ module GX::Models end end end - - diff --git a/src/models/gocryptfs_config.cr b/src/models/gocryptfs_config.cr index 3cbcd61..9013bdf 100644 --- a/src/models/gocryptfs_config.cr +++ b/src/models/gocryptfs_config.cr @@ -13,23 +13,23 @@ module GX::Models include Concerns::Base - def _mounted_prefix() + def _mounted_prefix "#{encrypted_path}" end - def mounted_name() + def mounted_name "#{@name}.Open" end - def _mount_action() + def _mount_action mount_point_safe = @mount_point raise InvalidMountpointError.new("Invalid mount point") if mount_point_safe.nil? process = Process.new( - "gocryptfs", - ["-idle", "15m", @encrypted_path, mount_point_safe], - input: STDIN, - output: STDOUT, + "gocryptfs", + ["-idle", "15m", @encrypted_path, mount_point_safe], + input: STDIN, + output: STDOUT, error: STDERR ) return process.wait diff --git a/src/models/httpdirfs_config.cr b/src/models/httpdirfs_config.cr index fdcf012..30db9eb 100644 --- a/src/models/httpdirfs_config.cr +++ b/src/models/httpdirfs_config.cr @@ -13,23 +13,23 @@ module GX::Models include Concerns::Base - def _mounted_prefix() + def _mounted_prefix "httpdirfs" end - def mounted_name() + def mounted_name @name end - def _mount_action() + def _mount_action mount_point_safe = @mount_point raise InvalidMountpointError.new("Invalid mount point") if mount_point_safe.nil? process = Process.new( - "httpdirfs", - ["#{@url}", mount_point_safe], - input: STDIN, - output: STDOUT, + "httpdirfs", + ["#{@url}", mount_point_safe], + input: STDIN, + output: STDOUT, error: STDERR ) return process.wait diff --git a/src/models/root_config.cr b/src/models/root_config.cr index c6de32b..d88dc61 100644 --- a/src/models/root_config.cr +++ b/src/models/root_config.cr @@ -12,13 +12,13 @@ module GX::Models # def self.from_yaml(ctx : YAML::ParseContext , node : YAML::Nodes::Node) # l_node = node # if l_node.is_a?(YAML::Nodes::Scalar) - # value_patched = Crinja.render(l_node.value, {"env" => ENV.to_h}) + # value_patched = Crinja.render(l_node.value, {"env" => ENV.to_h}) # return value_patched # end # return "" # end - # + # # def self.to_yaml(value, builder : YAML::Nodes::Builder) # end # end @@ -27,7 +27,7 @@ module GX::Models include YAML::Serializable include YAML::Serializable::Strict - # @[YAML::Field(key: "version", converter: GX::Models::CrinjaConverter)] + # @[YAML::Field(key: "version", converter: GX::Models::CrinjaConverter)] @[YAML::Field(key: "version")] getter version : String @@ -38,4 +38,3 @@ module GX::Models getter filesystems : Array(AbstractFilesystemConfig) end end - diff --git a/src/models/sshfs_config.cr b/src/models/sshfs_config.cr index fee42e5..99da441 100644 --- a/src/models/sshfs_config.cr +++ b/src/models/sshfs_config.cr @@ -16,27 +16,27 @@ module GX::Models include Concerns::Base - def _mounted_prefix() + def _mounted_prefix "#{@remote_user}@#{@remote_host}:#{@remote_path}" end - def mounted_name() + def mounted_name @name end - def _mount_action() + def _mount_action mount_point_safe = @mount_point raise InvalidMountpointError.new("Invalid mount point") if mount_point_safe.nil? process = Process.new( - "sshfs", + "sshfs", [ "-p", remote_port, - "#{@remote_user}@#{@remote_host}:#{@remote_path}", - mount_point_safe - ], - input: STDIN, - output: STDOUT, + "#{@remote_user}@#{@remote_host}:#{@remote_path}", + mount_point_safe, + ], + input: STDIN, + output: STDOUT, error: STDERR ) return process.wait diff --git a/src/parsers/base.cr b/src/parsers/base.cr new file mode 100644 index 0000000..8f3573b --- /dev/null +++ b/src/parsers/base.cr @@ -0,0 +1,5 @@ +module GX::Parsers + abstract class AbstractParser + abstract def build(parser : OptionParser, ancestors : BreadCrumbs, config : Config) + end +end diff --git a/src/parsers/completion_parser.cr b/src/parsers/completion_parser.cr new file mode 100644 index 0000000..1cdc4cc --- /dev/null +++ b/src/parsers/completion_parser.cr @@ -0,0 +1,26 @@ +require "./base.cr" + +module GX::Parsers + class CompletionParser < AbstractParser + def build(parser, ancestors, config) + breadcrumbs = ancestors + "completion" + + parser.banner = Utils.usage_line( + breadcrumbs, + "Manage #{PROGRAM_NAME} completion", + true + ) + parser.separator("\nCompletion commands:") + + parser.on("--bash", "Generate bash completion") do |flag| + Log.info { "Set bash completion" } + end + + parser.on("--zsh", "Generate zsh completion") do |flag| + Log.info { "Set zsh completion" } + end + + parser.separator Utils.help_line(breadcrumbs) + end + end +end diff --git a/src/parsers/config_parser.cr b/src/parsers/config_parser.cr new file mode 100644 index 0000000..b902cd5 --- /dev/null +++ b/src/parsers/config_parser.cr @@ -0,0 +1,38 @@ +require "./options/config_options" +require "./options/config_init_options" +require "./base" +require "../types/modes" +require "../utils/parser_lines" + +module GX::Parsers + class ConfigParser < AbstractParser + def build(parser, ancestors, config) + breadcrumbs = ancestors + "config" + config.config_options = Parsers::Options::ConfigOptions.new + parser.banner = Utils.usage_line( + breadcrumbs, + "Helpers for #{PROGRAM_NAME}'s configuration file", + true + ) + parser.separator("\nConfig commands") + + parser.on("init", "Create initial mfm configuration") do + config.mode = Types::Mode::ConfigInit + config.config_init_options = Parsers::Options::ConfigInitOptions.new + + 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| + config.config_init_options.try do |opts| + opts.path = path + end + end + + parser.separator(Utils.help_line(breadcrumbs + "init")) + end + + parser.separator(Utils.help_line(breadcrumbs)) + end + end +end diff --git a/src/parsers/mapping_parser.cr b/src/parsers/mapping_parser.cr new file mode 100644 index 0000000..fc972a9 --- /dev/null +++ b/src/parsers/mapping_parser.cr @@ -0,0 +1,73 @@ +require "./base.cr" +require "../utils/parser_lines" + +module GX::Parsers + class MappingParser < AbstractParser + def build(parser, ancestors, config) + breadcrumbs = ancestors + "mapping" + add_args = {name: "", path: ""} + delete_args = {name: ""} + + parser.banner = Utils.usage_line( + breadcrumbs, + "Manage FUSE filesystem mappings", + true + ) + parser.separator("\nCommands") + + 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 + 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}) + end + parser.on("-p", "--path", "Set vault encrypted path") do |path| + add_args = add_args.merge({path: path}) + end + parser.separator(Utils.help_line(breadcrumbs + "create")) + end + + parser.on("edit", "Edit configuration") do |flag| + config.mode = Types::Mode::MappingEdit + parser.separator(Utils.help_line(breadcrumbs + "edit")) + # abort("FIXME: Not implemented") + end + + parser.on("mount", "Mount mapping") do |flag| + config.mode = Types::Mode::MappingMount + parser.separator(Utils.help_line(breadcrumbs + "mount")) + # abort("FIXME: Not implemented") + end + + parser.on("umount", "Umount mapping") do |flag| + config.mode = Types::Mode::MappingUmount + 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.on("-n", "--name", "Set vault name") do |name| + delete_args = delete_args.merge({name: name}) + end + parser.separator(Utils.help_line(breadcrumbs + "delete")) + end + + parser.separator Utils.help_line(breadcrumbs) + end + end +end diff --git a/src/parsers/options/config_init_options.cr b/src/parsers/options/config_init_options.cr new file mode 100644 index 0000000..97a31a3 --- /dev/null +++ b/src/parsers/options/config_init_options.cr @@ -0,0 +1,7 @@ +require "option_parser" + +module GX::Parsers::Options + class ConfigInitOptions + property path : String? + end +end diff --git a/src/parsers/options/config_options.cr b/src/parsers/options/config_options.cr new file mode 100644 index 0000000..6cacac0 --- /dev/null +++ b/src/parsers/options/config_options.cr @@ -0,0 +1,6 @@ +require "option_parser" + +module GX::Parsers::Options + class ConfigOptions + end +end diff --git a/src/parsers/options/help_options.cr b/src/parsers/options/help_options.cr new file mode 100644 index 0000000..e81fa2e --- /dev/null +++ b/src/parsers/options/help_options.cr @@ -0,0 +1,7 @@ +require "option_parser" + +module GX::Parsers::Options + class HelpOptions + property parser_snapshot : OptionParser? = nil + end +end diff --git a/src/parsers/root_parser.cr b/src/parsers/root_parser.cr new file mode 100644 index 0000000..70ac5a5 --- /dev/null +++ b/src/parsers/root_parser.cr @@ -0,0 +1,92 @@ +require "./base" +require "./config_parser" +require "./mapping_parser" +require "./completion_parser" +require "../utils/parser_lines" +require "../commands" + +module GX::Parsers + class RootParser < AbstractParser + def build(parser, ancestors, config) + breadcrumbs = ancestors + (File.basename PROGRAM_NAME) + + parser.banner = Utils.usage_line( + breadcrumbs, + "A management tool for your various FUSE filesystems", + true + ) + + parser.on("-c", "--config FILE", "Set configuration file") do |path| + Log.info { "Configuration set to #{path}" } + config.path = path + end + + parser.on("-v", "--verbose", "Set more verbosity") do |flag| + Log.info { "Verbosity enabled" } + config.verbose = true + end + + parser.on("-o", "--open", "Automatically open directory after mount") do |flag| + Log.info { "Auto-open enabled" } + config.auto_open = true + end + + parser.on("--version", "Show version") do |flag| + config.mode = Types::Mode::GlobalVersion + end + + parser.on("-h", "--help", "Show this help") do |flag| + config.mode = Types::Mode::GlobalHelp + config.help_options = Parsers::Options::HelpOptions.new + config.help_options.try { |opts| opts.parser_snapshot = parser.dup } + end + + parser.separator("\nGlobal commands:") + + 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.command = Commands::Config.new(config) + Parsers::ConfigParser.new.build(parser, breadcrumbs, config) + end + + parser.on("tui", "Interactive text user interface (default)") do + config.mode = Types::Mode::GlobalTui + end + + 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 } + + Parsers::MappingParser.new.build(parser, breadcrumbs, config) + end + + # parser.on("interactive", "Interactive mapping mount/umount") do + # abort("FIXME: Not implemented") + # end + + parser.on("completion", "Manage completion") do + config.mode = Types::Mode::GlobalCompletion + Parsers::CompletionParser.new.build(parser, breadcrumbs, config) + end + + parser.separator(Utils.help_line(breadcrumbs)) + + # Manage errors + parser.unknown_args do |remaining_args, _| + next if remaining_args.size == 0 + + puts parser + abort("ERROR: Invalid arguments: #{remaining_args.join(" ")}") + end + + parser.invalid_option do |ex| + puts parser + abort("ERROR: Invalid option: '#{ex}'!") + end + end + end +end diff --git a/src/types/modes.cr b/src/types/modes.cr new file mode 100644 index 0000000..6498f89 --- /dev/null +++ b/src/types/modes.cr @@ -0,0 +1,21 @@ +module GX::Types + enum Mode + None + + GlobalVersion + GlobalHelp + GlobalCompletion + GlobalTui + GlobalConfig + GlobalMapping + + ConfigInit + + MappingCreate + MappingDelete + MappingEdit + MappingList + MappingMount + MappingUmount + end +end diff --git a/src/utils/breadcrumbs.cr b/src/utils/breadcrumbs.cr new file mode 100644 index 0000000..89c54e9 --- /dev/null +++ b/src/utils/breadcrumbs.cr @@ -0,0 +1,19 @@ +module GX::Utils + class BreadCrumbs + def initialize(base : Array(String)) + @ancestors = base + end + + def +(elem : String) + b = BreadCrumbs.new(@ancestors + [elem]) + end + + def to_s + @ancestors.join(" ") + end + + def to_a + @ancestors.clone + end + end +end diff --git a/src/fzf.cr b/src/utils/fzf.cr similarity index 92% rename from src/fzf.cr rename to src/utils/fzf.cr index bd1acc2..ff626f3 100644 --- a/src/fzf.cr +++ b/src/utils/fzf.cr @@ -3,9 +3,8 @@ # SPDX-FileCopyrightText: 2023 Glenn Y. Rolland # Copyright © 2023 Glenn Y. Rolland -module GX +module GX::Utils class Fzf - def self.run(list : Array(String)) : String input = IO::Memory.new input.puts list.join("\n") @@ -29,8 +28,7 @@ module GX exit(1) end - result = output.to_s.strip #.split.first? + result = output.to_s.strip # .split.first? end end end - diff --git a/src/utils/parser_lines.cr b/src/utils/parser_lines.cr new file mode 100644 index 0000000..dacd71f --- /dev/null +++ b/src/utils/parser_lines.cr @@ -0,0 +1,17 @@ +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]", + "", + description, + "", + "Global options:", + ].join("\n") + end + + def self.help_line(breadcrumbs : BreadCrumbs) + "\nRun '#{breadcrumbs.to_s} COMMAND --help' for more information on a command." + end +end diff --git a/src/version.cr b/src/version.cr index 89bd308..0fd98ff 100644 --- a/src/version.cr +++ b/src/version.cr @@ -1,8 +1,5 @@ - require "version_from_shard" module GX VersionFromShard.declare end - - -- 2.45.2 From 6feedc2c70ab25fc791a6a2f00568401c32ebd44 Mon Sep 17 00:00:00 2001 From: Glenn Date: Sun, 14 Jan 2024 20:32:23 +0100 Subject: [PATCH 25/32] doc: update README according to code --- README.md | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cd7bfd8..254cb91 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ version](https://code.apps.glenux.net/glenux/mfm/releases) of MFM. ### Command Line Options +Global + ``` Usage: mfm [options] @@ -77,9 +79,43 @@ Global options -h, --help Show this help Commands (not implemented yet): - create Add a new filesystem - delete Remove an existing filesystem - edit Modify the configuration + config Manage configuration file + mapping Manage filesystems +``` + +Config management + +``` +Usage: mfm filesystem [options] + +Global options + -c, --config FILE Set configuration file + -v, --verbose Set more verbosity + -o, --open Automatically open directory after mount + --version Show version + -h, --help Show this help + +Commands (not implemented yet): + init Create init file +``` + +Filesystem management + +``` +Usage: mfm mapping [options] + +Global options + -c, --config FILE Set configuration file + -v, --verbose Set more verbosity + -o, --open Automatically open directory after mount + --version Show version + -h, --help Show this help + +Commands (not implemented yet): + list List fuse mappings + create Create new fuse mapping + edit Edit fuse mapping + delete Create new fuse mapping ``` ### Demo -- 2.45.2 From 32f0b6832bf05417aaebf8627f79676444fce9dd Mon Sep 17 00:00:00 2001 From: Glenn Date: Mon, 15 Jan 2024 02:04:25 +0100 Subject: [PATCH 26/32] feat: enable preview_mt flag --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3894aa9..f0c5bb8 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ prepare: shards install build: - shards build --error-trace + shards build --error-trace -Dpreview_mt @echo SUCCESS watch: -- 2.45.2 From 702f731d147ca40569fcadddf4ae2c0a8543d0bf Mon Sep 17 00:00:00 2001 From: Glenn Date: Mon, 15 Jan 2024 02:04:42 +0100 Subject: [PATCH 27/32] chore: improve code-preloader config --- .code_preloader.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.code_preloader.yml b/.code_preloader.yml index 5b28953..97920cc 100644 --- a/.code_preloader.yml +++ b/.code_preloader.yml @@ -8,8 +8,15 @@ # List of patterns to ignore during preloading ignore_list: - - ^\.git/.* + - ^\.git/ - ^lib.* + - ^doc/ + - ^bin/ + - ^_prompts/ + - ^\.reuse/ + - ^LICENSES/ + - ^\.vagrant/ + - ^scripts/ # Path to the output file (if null, output to STDOUT) output_path: null -- 2.45.2 From f6f320e389b4dead32344e5fb35a2446d6d8c608 Mon Sep 17 00:00:00 2001 From: Glenn Date: Mon, 15 Jan 2024 02:05:07 +0100 Subject: [PATCH 28/32] chore: ignore _* files/dirs --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ef729c3..ccd3fc2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # SPDX-FileCopyrightText: 2023 Glenn Y. Rolland # Copyright © 2023 Glenn Y. Rolland +/_* .vagrant bin lib -- 2.45.2 From 5b0655780ec8420a84e0bfb31bd4cb274c2b58b3 Mon Sep 17 00:00:00 2001 From: Glenn Date: Mon, 15 Jan 2024 02:05:30 +0100 Subject: [PATCH 29/32] feat: add dependency upon tablo --- shard.lock | 8 ++++++++ shard.yml | 23 ++++++++++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/shard.lock b/shard.lock index e13838b..e19d81c 100644 --- a/shard.lock +++ b/shard.lock @@ -1,5 +1,9 @@ version: 2.0 shards: + ameba: + git: https://github.com/crystal-ameba/ameba.git + version: 1.6.1 + crinja: git: https://github.com/straight-shoota/crinja.git version: 0.8.1 @@ -8,6 +12,10 @@ shards: git: https://github.com/sztheory/shellwords-crystal.git version: 0.1.0 + tablo: + git: https://github.com/hutou/tablo.git + version: 0.10.1 + version_from_shard: git: https://github.com/hugopl/version_from_shard.git version: 1.2.5 diff --git a/shard.yml b/shard.yml index 91274e8..3b29118 100644 --- a/shard.yml +++ b/shard.yml @@ -5,17 +5,17 @@ # Copyright © 2023 Glenn Y. Rolland name: Minimalist FUSE Manager -version: 0.1.11 +version: 0.2.0 targets: mfm: main: src/main.cr -# authors: -# - name +authors: + - Glenn Y. Rolland -# description: | -# Short description of gx-vault +description: | + FIXME. write description dependencies: crinja: @@ -24,14 +24,11 @@ dependencies: github: szTheory/shellwords-crystal version_from_shard: github: hugopl/version_from_shard + tablo: + github: hutou/tablo -# dependencies: -# pg: -# github: will/crystal-pg -# version: "~> 0.5" - -# development_dependencies: -# webmock: -# github: manastech/webmock.cr +development_dependencies: + ameba: + github: crystal-ameba/ameba license: GPL-3 -- 2.45.2 From 36fd93832579d31814222bfe7dad9284497957e1 Mon Sep 17 00:00:00 2001 From: Glenn Date: Mon, 15 Jan 2024 02:06:28 +0100 Subject: [PATCH 30/32] feat: add basic support for spec --- spec/spec_helper.cr | 1 + spec/utils/breadcrumbs_spec.cr | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 spec/spec_helper.cr create mode 100644 spec/utils/breadcrumbs_spec.cr diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr new file mode 100644 index 0000000..e2f4f80 --- /dev/null +++ b/spec/spec_helper.cr @@ -0,0 +1 @@ +require "spec" diff --git a/spec/utils/breadcrumbs_spec.cr b/spec/utils/breadcrumbs_spec.cr new file mode 100644 index 0000000..deec943 --- /dev/null +++ b/spec/utils/breadcrumbs_spec.cr @@ -0,0 +1,46 @@ +require "../spec_helper" +require "../../src/utils/breadcrumbs" + +describe GX::Utils::BreadCrumbs do + context "Initialization" do + it "can initialize from array" do + # empty string + b1 = GX::Utils::BreadCrumbs.new([] of String) + b1.to_a.should be_empty + + # simple string + b2 = GX::Utils::BreadCrumbs.new(["test1"]) + b2.to_a.should eq(["test1"]) + + # array + b3 = GX::Utils::BreadCrumbs.new(["test1", "test2"]) + b3.to_a.should eq(["test1", "test2"]) + end + end + + context "Functioning" do + it "can add values" do + # empty string + b1 = GX::Utils::BreadCrumbs.new([] of String) + b1.to_a.should be_empty + + # simple string + b2 = b1 + "test1" + b2.to_a.should eq(["test1"]) + + b3 = b2 + "test2" + b3.to_a.should eq(["test1", "test2"]) + end + + it "can become a string" do + b1 = GX::Utils::BreadCrumbs.new([] of String) + b1.to_s.should eq("") + + b2 = b1 + "test1" + b2.to_a.should eq("test1") + + b3 = b2 + "test2" + b3.to_a.should eq("test1 test2") + end + end +end -- 2.45.2 From 37710103ec57fcfd942e607d884fe13cc0cae744 Mon Sep 17 00:00:00 2001 From: glenux Date: Tue, 14 May 2024 07:33:09 +0000 Subject: [PATCH 31/32] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 254cb91..8e1a613 100644 --- a/README.md +++ b/README.md @@ -180,5 +180,5 @@ By contributing, you agree to our code of conduct and license terms. ## License -GNU GPL-3 +GNU GPL-3 -- 2.45.2 From 5f775ac45fb4144db2287c76dc69ff8142e6fa13 Mon Sep 17 00:00:00 2001 From: Glenn Date: Sat, 5 Oct 2024 10:54:40 +0200 Subject: [PATCH 32/32] feat: add support for sshfs option (-o) in config --- src/cli.cr | 2 +- src/main.cr | 6 +++--- src/models/sshfs_config.cr | 18 +++++++++++++----- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/cli.cr b/src/cli.cr index 4366d07..04fa16f 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -16,7 +16,7 @@ module GX class Cli Log = ::Log.for("cli") - @config : GX::Config + @config : GX::Config def initialize # Main execution starts here diff --git a/src/main.cr b/src/main.cr index 28e57a4..c02ff74 100644 --- a/src/main.cr +++ b/src/main.cr @@ -31,6 +31,6 @@ Log.setup do |config| end end -app = GX::Cli.new -app.parse_command_line(ARGV) -app.run +cli = GX::Cli.new +cli.parse_command_line(ARGV) +cli.run diff --git a/src/models/sshfs_config.cr b/src/models/sshfs_config.cr index 99da441..898ccdb 100644 --- a/src/models/sshfs_config.cr +++ b/src/models/sshfs_config.cr @@ -13,6 +13,7 @@ module GX::Models getter remote_user : String = "" getter remote_host : String = "" getter remote_port : String = "22" + getter options : Array(String) = [] of String include Concerns::Base @@ -28,13 +29,19 @@ module GX::Models mount_point_safe = @mount_point raise InvalidMountpointError.new("Invalid mount point") if mount_point_safe.nil? + options = [] of String + # merge sshfs options + @options.each do |option| + options.push("-o", option) + end + options.push("-p", remote_port) + options.push( + "#{@remote_user}@#{@remote_host}:#{@remote_path}", + mount_point_safe + ) process = Process.new( "sshfs", - [ - "-p", remote_port, - "#{@remote_user}@#{@remote_host}:#{@remote_path}", - mount_point_safe, - ], + options, input: STDIN, output: STDOUT, error: STDERR @@ -43,3 +50,4 @@ module GX::Models end end end + -- 2.45.2