feat: implement local & global mount_point definition
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

This commit is contained in:
Glenn Y. Rolland 2023-11-24 00:20:16 +01:00
parent ee3f57ec20
commit 23d4def217
17 changed files with 334 additions and 257 deletions

View file

@ -92,13 +92,18 @@ module GX
STDOUT.puts "#{PROGRAM_NAME} #{VERSION}" STDOUT.puts "#{PROGRAM_NAME} #{VERSION}"
when Config::Mode::Mount when Config::Mode::Mount
@config.load_from_file @config.load_from_file
mount filesystem = choose_filesystem
mount_or_umount(filesystem) if !filesystem.nil?
end end
end end
def mount() def choose_filesystem()
names_display = {} of String => NamedTuple(filesystem: Filesystem::AbstractFilesystem, ansi_name: String) names_display = {} of String => NamedTuple(filesystem: Models::AbstractFilesystemConfig, ansi_name: String)
@config.filesystems.each do |filesystem|
config_root = @config.root
return if config_root.nil?
config_root.filesystems.each do |filesystem|
fs_str = filesystem.type.ljust(12,' ') fs_str = filesystem.type.ljust(12,' ')
suffix = "" suffix = ""
@ -121,12 +126,22 @@ module GX
selected_filesystem = names_display[result_filesystem_name][:filesystem] selected_filesystem = names_display[result_filesystem_name][:filesystem]
puts ">> #{selected_filesystem.name}".colorize(:yellow) puts ">> #{selected_filesystem.name}".colorize(:yellow)
if selected_filesystem if !selected_filesystem
selected_filesystem.mounted? ? selected_filesystem.unmount : selected_filesystem.mount
else
STDERR.puts "Vault not found: #{selected_filesystem}.".colorize(:red) STDERR.puts "Vault not found: #{selected_filesystem}.".colorize(:red)
return
end 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 end
end end

View file

@ -5,7 +5,7 @@
require "crinja" require "crinja"
require "./filesystems" require "./models"
module GX module GX
class Config class Config
@ -23,9 +23,10 @@ module GX
record AddArgs, name : String, path : String record AddArgs, name : String, path : String
record DelArgs, name : String record DelArgs, name : String
getter filesystems : Array(Filesystem::AbstractFilesystem) # getter filesystems : Array(Models::AbstractFilesystemConfig)
getter home_dir : String getter home_dir : String
getter global_mount_point : String? getter root : Models::RootConfig?
property verbose : Bool property verbose : Bool
property mode : Mode property mode : Mode
property path : String? property path : String?
@ -39,14 +40,13 @@ module GX
@verbose = false @verbose = false
@mode = Mode::Mount @mode = Mode::Mount
@filesystems = [] of Filesystem::AbstractFilesystem @filesystems = [] of Models::AbstractFilesystemConfig
@path = nil @path = nil
@global_mount_point = nil
@args = NoArgs @args = NoArgs
end end
def detect_config_file() private def detect_config_file()
possible_files = [ possible_files = [
File.join(@home_dir, ".config", "mfm", "config.yaml"), File.join(@home_dir, ".config", "mfm", "config.yaml"),
File.join(@home_dir, ".config", "mfm", "config.yml"), File.join(@home_dir, ".config", "mfm", "config.yml"),
@ -70,50 +70,32 @@ module GX
end end
def load_from_file def load_from_file
path = @path config_path = @path
if path.nil? if config_path.nil?
path = detect_config_file() config_path = detect_config_file()
end end
@path = path @path = config_path
@filesystems = [] of Filesystem::AbstractFilesystem
if !File.exists? path if !File.exists? config_path
Log.error { "File #{path} does not exist!".colorize(:red) } Log.error { "File #{path} does not exist!".colorize(:red) }
exit(1) exit(1)
end 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_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})
yaml_data = YAML.parse(file_patched) root = Models::RootConfig.from_yaml(file_patched)
# Extract schema version global_mount_point = root.global.mount_point
if yaml_data["version"]? raise "Invalid global mount point" if global_mount_point.nil?
schema_version = yaml_data["version"].as_s?
end
# Extract global settings root.filesystems.each do |selected_filesystem|
if yaml_data["global"]?.try &.as_h? if !selected_filesystem.mount_point?
global_data = yaml_data["global"] selected_filesystem.mount_point =
if global_data["mountpoint"]? File.join(global_mount_point, selected_filesystem.mounted_name)
@global_mount_point = global_data["mountpoint"].as_s?
end end
end end
@root = root
# 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
end end
end end
end end

View file

@ -1,9 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "./filesystems/gocryptfs"
require "./filesystems/sshfs"
require "./filesystems/httpdirfs"
require "./filesystems/abstract_filesystem"

View file

@ -1,27 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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

View file

@ -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

View file

@ -1,42 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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

View file

@ -1,43 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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

View file

@ -1,49 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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

View file

@ -8,7 +8,6 @@ require "colorize"
require "json" require "json"
require "log" require "log"
require "./filesystems/gocryptfs"
require "./config" require "./config"
require "./cli" require "./cli"
@ -32,7 +31,6 @@ Log.setup do |config|
end end
end end
app = GX::Cli.new app = GX::Cli.new
app.parse_command_line(ARGV) app.parse_command_line(ARGV)
app.run app.run

11
src/models.cr Normal file
View file

@ -0,0 +1,11 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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"

View file

@ -0,0 +1,32 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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

View file

@ -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

View file

@ -0,0 +1,28 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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

View file

@ -0,0 +1,41 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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

View file

@ -0,0 +1,41 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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

41
src/models/root_config.cr Normal file
View file

@ -0,0 +1,41 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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 "<null>"
# 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

View file

@ -0,0 +1,48 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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