Compare commits
24 commits
1697750abd
...
9cce357dd0
Author | SHA1 | Date | |
---|---|---|---|
9cce357dd0 | |||
fb168d5308 | |||
4730c77992 | |||
9f3f3b24c1 | |||
041550cc0f | |||
fd9829c283 | |||
70b51527df | |||
58e4ab05bf | |||
d4c52cd044 | |||
32fea233d1 | |||
84230a6828 | |||
211419ea02 | |||
5107e80aa7 | |||
7f789daefa | |||
cb14a04fbe | |||
63c0bbbb1c | |||
8fc9f2cfda | |||
23d4def217 | |||
ee3f57ec20 | |||
8efe8ea5d9 | |||
587bff04ca | |||
994f9e1885 | |||
8f2c2442a3 | |||
d91e9a8fcd |
32 changed files with 611 additions and 249 deletions
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
@ -0,0 +1 @@
|
||||||
|
crystal 1.10.1
|
6
Makefile
6
Makefile
|
@ -6,4 +6,8 @@
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
shards build
|
shards build --error-trace
|
||||||
|
@echo SUCCESS
|
||||||
|
|
||||||
|
watch:
|
||||||
|
watchexec --restart --delay-run 3 -c -e cr make build
|
||||||
|
|
16
README.md
16
README.md
|
@ -62,9 +62,12 @@ version](https://code.apps.glenux.net/glenux/mfm/releases) of MFM.
|
||||||
```
|
```
|
||||||
Usage: mfm [options]
|
Usage: mfm [options]
|
||||||
|
|
||||||
Global options:
|
Global options
|
||||||
-c, --config FILE Specify configuration file
|
-c, --config FILE Set configuration file
|
||||||
-h, --help Display this help
|
-v, --verbose Set more verbosity
|
||||||
|
-o, --open Automatically open directory after mount
|
||||||
|
--version Show version
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
Commands (not implemented yet):
|
Commands (not implemented yet):
|
||||||
create Add a new filesystem
|
create Add a new filesystem
|
||||||
|
@ -84,19 +87,20 @@ detail the filesystem names, types, and respective configurations.
|
||||||
### YAML File Format
|
### YAML File Format
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
---
|
||||||
version: "1"
|
version: "1"
|
||||||
|
|
||||||
global:
|
global:
|
||||||
mountpoint: "/home/user/mnt/{{name}}"
|
mountpoint: "{{env.HOME}}/mnt"
|
||||||
|
|
||||||
filesystems:
|
filesystems:
|
||||||
- type: "gocryptfs"
|
- type: "gocryptfs"
|
||||||
name: "Work - SSH Keys"
|
name: "Work - SSH Keys"
|
||||||
encrypted_path: "/home/user/.ssh/keyring.work"
|
encrypted_path: "/home/user/.ssh/keyring.work.vault"
|
||||||
|
|
||||||
- type: "sshfs"
|
- type: "sshfs"
|
||||||
name: "Personal - Media Server"
|
name: "Personal - Media Server"
|
||||||
remote_user: "user"
|
remote_user: "{{env.USER}}"
|
||||||
remote_host: "mediaserver.local"
|
remote_host: "mediaserver.local"
|
||||||
remote_path: "/mnt/largedisk/music"
|
remote_path: "/mnt/largedisk/music"
|
||||||
remote_port: 22
|
remote_port: 22
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
version: 1
|
version: 1
|
||||||
|
|
||||||
global:
|
global:
|
||||||
mountpoint: "~/mnt"
|
mountpoint: "{{env.HOME}}/mnt"
|
||||||
|
|
||||||
filesystems:
|
filesystems:
|
||||||
- type: gocryptfs
|
- type: gocryptfs
|
||||||
|
@ -16,7 +15,7 @@ filesystems:
|
||||||
|
|
||||||
- type: sshfs
|
- type: sshfs
|
||||||
name: "Personal - Remote Media Server"
|
name: "Personal - Remote Media Server"
|
||||||
remote_user: user
|
remote_user: "{{env.USER}}"
|
||||||
remote_host: mediaserver.local
|
remote_host: mediaserver.local
|
||||||
remote_port: 22
|
remote_port: 22
|
||||||
remote_path: "/remote/path/to/media"
|
remote_path: "/remote/path/to/media"
|
||||||
|
|
86
src/cli.cr
86
src/cli.cr
|
@ -40,6 +40,11 @@ module GX
|
||||||
@config.verbose = true
|
@config.verbose = true
|
||||||
end
|
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|
|
parser.on("--version", "Show version") do |flag|
|
||||||
@config.mode = Config::Mode::ShowVersion
|
@config.mode = Config::Mode::ShowVersion
|
||||||
end
|
end
|
||||||
|
@ -94,13 +99,69 @@ 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
|
||||||
|
raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil?
|
||||||
|
|
||||||
|
mount_or_umount(filesystem)
|
||||||
|
auto_open(filesystem) if filesystem.mounted? && @config.auto_open
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def mount()
|
def auto_open(filesystem)
|
||||||
names_display = {} of String => NamedTuple(filesystem: Filesystem, ansi_name: String)
|
# FIXME: support xdg-open
|
||||||
@config.filesystems.each do |filesystem|
|
# 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
|
||||||
|
|
||||||
|
alias FsTuple = NamedTuple(
|
||||||
|
filesystem: Models::Filesystems::AbstractConfig,
|
||||||
|
ansi_name: String
|
||||||
|
)
|
||||||
|
|
||||||
|
def choose_filesystem()
|
||||||
|
names_display = {} of String => FsTuple
|
||||||
|
|
||||||
|
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 = ""
|
||||||
|
@ -119,16 +180,25 @@ module GX
|
||||||
}
|
}
|
||||||
end
|
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]
|
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
|
||||||
|
return selected_filesystem
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mount_or_umount(selected_filesystem)
|
||||||
|
if !selected_filesystem.mounted?
|
||||||
|
selected_filesystem.mount()
|
||||||
|
else
|
||||||
|
selected_filesystem.umount()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,12 +5,15 @@
|
||||||
|
|
||||||
require "crinja"
|
require "crinja"
|
||||||
|
|
||||||
require "./filesystems"
|
require "./models"
|
||||||
|
|
||||||
module GX
|
module GX
|
||||||
class Config
|
class Config
|
||||||
Log = ::Log.for("config")
|
Log = ::Log.for("config")
|
||||||
|
|
||||||
|
class MissingFileError < Exception
|
||||||
|
end
|
||||||
|
|
||||||
enum Mode
|
enum Mode
|
||||||
ConfigAdd
|
ConfigAdd
|
||||||
ConfigDelete
|
ConfigDelete
|
||||||
|
@ -23,28 +26,30 @@ 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)
|
|
||||||
getter home_dir : String
|
getter home_dir : String
|
||||||
|
getter root : Models::RootConfig?
|
||||||
|
|
||||||
property verbose : Bool
|
property verbose : Bool
|
||||||
property mode : Mode
|
property mode : Mode
|
||||||
property path : String?
|
property path : String?
|
||||||
property args : AddArgs.class | DelArgs.class | NoArgs.class
|
property args : AddArgs.class | DelArgs.class | NoArgs.class
|
||||||
|
property auto_open : Bool
|
||||||
|
|
||||||
def initialize()
|
def initialize()
|
||||||
if !ENV["HOME"]?
|
raise Models::InvalidEnvironmentError.new("Home directory not found") if !ENV["HOME"]?
|
||||||
raise "Home directory not found"
|
|
||||||
end
|
|
||||||
@home_dir = ENV["HOME"]
|
@home_dir = ENV["HOME"]
|
||||||
|
|
||||||
@verbose = false
|
@verbose = false
|
||||||
|
@auto_open = false
|
||||||
|
|
||||||
@mode = Mode::Mount
|
@mode = Mode::Mount
|
||||||
@filesystems = [] of Filesystem
|
@filesystems = [] of Models::Filesystems::AbstractConfig
|
||||||
@path = nil
|
@path = 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"),
|
||||||
|
@ -56,47 +61,44 @@ module GX
|
||||||
|
|
||||||
possible_files.each do |file_path|
|
possible_files.each do |file_path|
|
||||||
if File.exists?(file_path)
|
if File.exists?(file_path)
|
||||||
Log.info { "Configuration file found: #{file_path}" }
|
Log.info { "Configuration file found: #{file_path}" } if @verbose
|
||||||
return file_path if File.exists?(file_path)
|
return file_path if File.exists?(file_path)
|
||||||
else
|
else
|
||||||
Log.debug { "Configuration file not found: #{file_path}" }
|
Log.debug { "Configuration file not found: #{file_path}" } if @verbose
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Log.error { "No configuration file found in any of the standard locations" }
|
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
|
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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
private def load_filesystems(config_path : String)
|
|
||||||
file_data = File.read(config_path)
|
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})
|
file_patched = Crinja.render(file_data, {"env" => ENV.to_h})
|
||||||
|
|
||||||
yaml_data = YAML.parse(file_patched)
|
root = Models::RootConfig.from_yaml(file_patched)
|
||||||
vaults_data = yaml_data["filesystems"].as_a
|
|
||||||
|
|
||||||
vaults_data.each do |filesystem_data|
|
mount_point_base_safe = root.global.mount_point_base
|
||||||
type = filesystem_data["type"].as_s
|
raise Models::InvalidMountpointError.new("Invalid global mount point") if mount_point_base_safe.nil?
|
||||||
name = filesystem_data["name"].as_s
|
|
||||||
# encrypted_path = filesystem_data["encrypted_path"].as_s
|
root.filesystems.each do |selected_filesystem|
|
||||||
@filesystems << Filesystem.from_yaml(filesystem_data.to_yaml)
|
if !selected_filesystem.mount_point?
|
||||||
# @filesystems << Filesystem.new(name, encrypted_path, "#{name}.Open")
|
selected_filesystem.mount_point =
|
||||||
end
|
File.join(mount_point_base_safe, selected_filesystem.mounted_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@root = root
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,48 +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
|
|
||||||
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"
|
|
|
@ -1,47 +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 "./filesystem"
|
|
||||||
|
|
||||||
module GX
|
|
||||||
class GoCryptFS < Filesystem
|
|
||||||
getter name : String = ""
|
|
||||||
getter encrypted_path : String = ""
|
|
||||||
|
|
||||||
@[YAML::Field(key: "mount_dir", ignore: true)]
|
|
||||||
getter mount_dir : String = ""
|
|
||||||
|
|
||||||
include GenericFilesystem
|
|
||||||
|
|
||||||
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 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
|
|
||||||
end
|
|
|
@ -1,48 +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 "./filesystem"
|
|
||||||
|
|
||||||
module GX
|
|
||||||
class HttpDirFS < Filesystem
|
|
||||||
getter name : String = ""
|
|
||||||
getter url : String = ""
|
|
||||||
|
|
||||||
@[YAML::Field(key: "mount_dir", ignore: true)]
|
|
||||||
getter mount_dir : String = ""
|
|
||||||
|
|
||||||
include GenericFilesystem
|
|
||||||
|
|
||||||
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}")
|
|
||||||
end
|
|
||||||
|
|
||||||
def mount
|
|
||||||
super do
|
|
||||||
input = STDIN
|
|
||||||
output = STDOUT
|
|
||||||
error = STDERR
|
|
||||||
process = Process.new(
|
|
||||||
"httpdirfs",
|
|
||||||
["#{url}", mount_dir],
|
|
||||||
input: input,
|
|
||||||
output: output,
|
|
||||||
error: error
|
|
||||||
)
|
|
||||||
unless process.wait.success?
|
|
||||||
puts "Error mounting the filesystem".colorize(:red)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
|
@ -1,54 +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 "./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"
|
|
||||||
|
|
||||||
@[YAML::Field(key: "mount_dir", ignore: true)]
|
|
||||||
getter mount_dir : String = ""
|
|
||||||
|
|
||||||
include GenericFilesystem
|
|
||||||
|
|
||||||
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 mount
|
|
||||||
super do
|
|
||||||
input = STDIN
|
|
||||||
output = STDOUT
|
|
||||||
error = STDERR
|
|
||||||
process = Process.new(
|
|
||||||
"sshfs",
|
|
||||||
[
|
|
||||||
"-p", remote_port,
|
|
||||||
"#{remote_user}@#{remote_host}:#{remote_path}",
|
|
||||||
mount_dir
|
|
||||||
],
|
|
||||||
input: input,
|
|
||||||
output: output,
|
|
||||||
error: error
|
|
||||||
)
|
|
||||||
unless process.wait.success?
|
|
||||||
puts "Error mounting the filesystem".colorize(:red)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -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
|
||||||
|
|
12
src/models.cr
Normal file
12
src/models.cr
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# 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/filesystems/gocryptfs_config"
|
||||||
|
require "./models/filesystems/sshfs_config"
|
||||||
|
require "./models/filesystems/httpdirfs_config"
|
||||||
|
require "./models/filesystems/abstract_config"
|
||||||
|
require "./models/exceptions"
|
15
src/models/exceptions.cr
Normal file
15
src/models/exceptions.cr
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
# 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
|
||||||
|
class InvalidFilesystemError < Exception
|
||||||
|
end
|
||||||
|
|
||||||
|
class InvalidMountpointError < Exception
|
||||||
|
end
|
||||||
|
end
|
32
src/models/filesystems/abstract_config.cr
Normal file
32
src/models/filesystems/abstract_config.cr
Normal 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::Filesystems
|
||||||
|
abstract class AbstractConfig
|
||||||
|
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
|
14
src/models/filesystems/concerns/mount.cr
Normal file
14
src/models/filesystems/concerns/mount.cr
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# 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>
|
||||||
|
|
||||||
|
module GX::Models::Concerns
|
||||||
|
module Mount
|
||||||
|
def mount()
|
||||||
|
_mount_wrapper() do
|
||||||
|
_mount_action
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,7 +3,12 @@
|
||||||
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
|
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
|
||||||
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
|
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
|
||||||
|
|
||||||
require "./filesystems/gocryptfs"
|
module GX::Models::Concerns
|
||||||
require "./filesystems/sshfs"
|
module MountPoint
|
||||||
require "./filesystems/httpdirfs"
|
def mount_point?()
|
||||||
require "./filesystems/filesystem"
|
!mount_point.nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
29
src/models/filesystems/concerns/mount_wrapper.cr
Normal file
29
src/models/filesystems/concerns/mount_wrapper.cr
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# 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>
|
||||||
|
|
||||||
|
module GX::Models::Concerns
|
||||||
|
module MountWrapper
|
||||||
|
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
|
||||||
|
|
||||||
|
result_status = yield
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
17
src/models/filesystems/concerns/mounted.cr
Normal file
17
src/models/filesystems/concerns/mounted.cr
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# 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>
|
||||||
|
|
||||||
|
module GX::Models::Concerns
|
||||||
|
module Mounted
|
||||||
|
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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
23
src/models/filesystems/concerns/umount.cr
Normal file
23
src/models/filesystems/concerns/umount.cr
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# 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>
|
||||||
|
|
||||||
|
module GX::Models::Concerns
|
||||||
|
module Umount
|
||||||
|
def umount() : Nil
|
||||||
|
mount_point_safe = @mount_point
|
||||||
|
raise InvalidMountpointError.new("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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
46
src/models/filesystems/gocryptfs_config.cr
Normal file
46
src/models/filesystems/gocryptfs_config.cr
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# 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_config"
|
||||||
|
require "./concerns/mount_wrapper"
|
||||||
|
require "./concerns/mounted"
|
||||||
|
require "./concerns/mount"
|
||||||
|
require "./concerns/umount"
|
||||||
|
require "./concerns/mount_point"
|
||||||
|
|
||||||
|
module GX::Models::Filesystems
|
||||||
|
class GoCryptFSConfig < AbstractConfig
|
||||||
|
getter encrypted_path : String = ""
|
||||||
|
|
||||||
|
include Concerns::Mount
|
||||||
|
include Concerns::Mounted
|
||||||
|
include Concerns::MountPoint
|
||||||
|
include Concerns::MountWrapper
|
||||||
|
include Concerns::Umount
|
||||||
|
|
||||||
|
def _mounted_prefix()
|
||||||
|
"#{encrypted_path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def mounted_name()
|
||||||
|
"#{@name}.Open"
|
||||||
|
end
|
||||||
|
|
||||||
|
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,
|
||||||
|
error: STDERR
|
||||||
|
)
|
||||||
|
return process.wait
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
47
src/models/filesystems/httpdirfs_config.cr
Normal file
47
src/models/filesystems/httpdirfs_config.cr
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# 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_config"
|
||||||
|
require "./concerns/mount_wrapper"
|
||||||
|
require "./concerns/mounted"
|
||||||
|
require "./concerns/mount"
|
||||||
|
require "./concerns/umount"
|
||||||
|
require "./concerns/mount_point"
|
||||||
|
|
||||||
|
module GX::Models::Filesystems
|
||||||
|
class HttpDirFSConfig < AbstractConfig
|
||||||
|
getter url : String = ""
|
||||||
|
|
||||||
|
include Concerns::Mount
|
||||||
|
include Concerns::Mounted
|
||||||
|
include Concerns::MountPoint
|
||||||
|
include Concerns::MountWrapper
|
||||||
|
include Concerns::Umount
|
||||||
|
|
||||||
|
def _mounted_prefix()
|
||||||
|
"httpdirfs"
|
||||||
|
end
|
||||||
|
|
||||||
|
def mounted_name()
|
||||||
|
@name
|
||||||
|
end
|
||||||
|
|
||||||
|
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,
|
||||||
|
error: STDERR
|
||||||
|
)
|
||||||
|
return process.wait
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
53
src/models/filesystems/sshfs_config.cr
Normal file
53
src/models/filesystems/sshfs_config.cr
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# 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_config"
|
||||||
|
require "./concerns/mount_wrapper"
|
||||||
|
require "./concerns/mounted"
|
||||||
|
require "./concerns/mount"
|
||||||
|
require "./concerns/umount"
|
||||||
|
require "./concerns/mount_point"
|
||||||
|
|
||||||
|
module GX::Models::Filesystems
|
||||||
|
class SshFSConfig < AbstractConfig
|
||||||
|
getter remote_path : String = ""
|
||||||
|
getter remote_user : String = ""
|
||||||
|
getter remote_host : String = ""
|
||||||
|
getter remote_port : String = "22"
|
||||||
|
|
||||||
|
include Concerns::Mount
|
||||||
|
include Concerns::Mounted
|
||||||
|
include Concerns::MountPoint
|
||||||
|
include Concerns::MountWrapper
|
||||||
|
include Concerns::Umount
|
||||||
|
|
||||||
|
def _mounted_prefix()
|
||||||
|
"#{@remote_user}@#{@remote_host}:#{@remote_path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def mounted_name()
|
||||||
|
@name
|
||||||
|
end
|
||||||
|
|
||||||
|
def _mount_action()
|
||||||
|
mount_point_safe = @mount_point
|
||||||
|
raise InvalidMountpointError.new("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
|
||||||
|
)
|
||||||
|
return process.wait
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
32
src/models/global_config.cr
Normal file
32
src/models/global_config.cr
Normal 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"
|
||||||
|
require "./filesystems/abstract_config"
|
||||||
|
|
||||||
|
module GX::Models
|
||||||
|
class InvalidEnvironmentError < Exception
|
||||||
|
end
|
||||||
|
|
||||||
|
class GlobalConfig
|
||||||
|
include YAML::Serializable
|
||||||
|
include YAML::Serializable::Strict
|
||||||
|
|
||||||
|
@[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_base.nil? || @mount_point_base.try &.empty?
|
||||||
|
@mount_point_base = File.join(home_dir, "mnt")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
41
src/models/root_config.cr
Normal file
41
src/models/root_config.cr
Normal 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 "./filesystems/abstract_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(Filesystems::AbstractConfig)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
14
src/operations/filesystems/create_command.cr
Normal file
14
src/operations/filesystems/create_command.cr
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# 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 "../abstract_command"
|
||||||
|
|
||||||
|
module GX::Operations::Filesystems
|
||||||
|
class CreateCommand < AbstractCommand
|
||||||
|
def execute()
|
||||||
|
raise NotImplementedError.new("CreateCommand is not Implemented")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
src/operations/filesystems/delete_command.cr
Normal file
14
src/operations/filesystems/delete_command.cr
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# 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 "../abstract_command"
|
||||||
|
|
||||||
|
module GX::Operations::Filesystems
|
||||||
|
class DeleteCommand < AbstractCommand
|
||||||
|
def execute()
|
||||||
|
raise NotImplementedError.new("DeleteCommand is not Implemented")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
src/operations/filesystems/edit_command.cr
Normal file
14
src/operations/filesystems/edit_command.cr
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# 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 "../abstract_command"
|
||||||
|
|
||||||
|
module GX::Operations::Filesystems
|
||||||
|
class EditCommand < AbstractCommand
|
||||||
|
def execute()
|
||||||
|
raise NotImplementedError.new("EditCommand is not Implemented")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
src/operations/filesystems/list_command.cr
Normal file
15
src/operations/filesystems/list_command.cr
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# 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 "../abstract_command"
|
||||||
|
|
||||||
|
module GX::Operations::Filesystems
|
||||||
|
class ListCommand < AbstractCommand
|
||||||
|
def execute()
|
||||||
|
raise NotImplementedError.new("ListCommand is not Implemented")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
14
src/operations/filesystems/mount_command.cr
Normal file
14
src/operations/filesystems/mount_command.cr
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# 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 "../abstract_command"
|
||||||
|
|
||||||
|
module GX::Operations::Filesystems
|
||||||
|
class MountCommand < AbstractCommand
|
||||||
|
def execute()
|
||||||
|
raise NotImplementedError.new("MountCommand is not Implemented")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
src/operations/filesystems/select_command.cr
Normal file
15
src/operations/filesystems/select_command.cr
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# 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 "../abstract_command"
|
||||||
|
|
||||||
|
module GX::Operations::Filesystems
|
||||||
|
class SelectCommand < AbstractCommand
|
||||||
|
def execute()
|
||||||
|
raise NotImplementedError.new("SelectCommand is not Implemented")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
14
src/operations/filesystems/umount_command.cr
Normal file
14
src/operations/filesystems/umount_command.cr
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# 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 "../abstract_command"
|
||||||
|
|
||||||
|
module GX::Operations::Filesystems
|
||||||
|
class UmountCommand < AbstractCommand
|
||||||
|
def execute()
|
||||||
|
raise NotImplementedError.new("UmountCommand is not Implemented")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
src/operations/global/edit_command.cr
Normal file
15
src/operations/global/edit_command.cr
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# 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 "../abstract_command"
|
||||||
|
|
||||||
|
module GX::Operations::Global
|
||||||
|
class EditCommand < AbstractCommand
|
||||||
|
def execute()
|
||||||
|
raise NotImplementedError.new("EditCommand is not Implemented")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
Loading…
Add table
Reference in a new issue