Compare commits

..

No commits in common. "develop" and "v0.1.9" have entirely different histories.

65 changed files with 389 additions and 1735 deletions

View file

@ -1,32 +0,0 @@
---
# 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.*
- ^doc/
- ^bin/
- ^_prompts/
- ^\.reuse/
- ^LICENSES/
- ^\.vagrant/
- ^scripts/
# 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

View file

@ -5,7 +5,7 @@ name: default
steps:
- name: build:binary
image: crystallang/crystal:1.11.0-alpine
image: crystallang/crystal:1.7.3
environment:
PACKAGE_BASENAME: mfm_linux_amd64
volumes:
@ -13,16 +13,11 @@ steps:
path: /_cache
commands:
- pwd
# - |
# apt-get update && \
# apt-get install -y \
# cmake g++ \
# libevent-dev libpcre3-dev \
# libyaml-dev liblzma-dev
- apt-get update &&
apt-get install -y cmake g++ libevent-dev libpcre3-dev libyaml-dev
- shards install
- shards build --production --static
- strip bin/mfm
- ./bin/mfm --version
- mkdir -p /_cache/bin
- cp -r bin/mfm /_cache/bin/$PACKAGE_BASENAME
@ -79,17 +74,8 @@ steps:
# FIXME: handle multi-arch
# FIXME: publish only on tags
services:
- name: docker
image: docker:dind
privileged: true
volumes:
- name: dockersock
path: /var/run
volumes:
- name: cache
temp: {}
- name: dockersock
temp: {}
#

1
.gitignore vendored
View file

@ -3,7 +3,6 @@
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
/_*
.vagrant
bin
lib

View file

@ -1 +0,0 @@
crystal 1.10.1

View file

@ -3,29 +3,7 @@
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
PREFIX=/usr
all: build
prepare:
shards install
build:
shards build --error-trace -Dpreview_mt
@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
shards build

View file

@ -6,13 +6,6 @@
-->
[![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:
> <https://code.apps.glenux.net/glenux/mfm>.
# Minimalist Fuse Manager (MFM)
@ -26,27 +19,11 @@ Before using MFM, make sure the following tools are installed on your system:
- **sshfs**: <https://github.com/libfuse/sshfs>
- **httpdirfs**: <https://github.com/fangfufu/httpdirfs>
- **fzf**: <https://github.com/junegunn/fzf>
- libpcre3
- libevent-2.1
For Debian/Ubuntu you can use the following command:
```shell-session
$ sudo apt-get update && sudo apt-get install libpcre3 libevent-2.1-7 fzf gocryptfs httpdirfs sshfs
```
## Building from source
To build from source, you'll also need:
- **crystal-lang**: <https://crystal-lang.org/>
For Debian/Ubuntu you can use the following command:
```shell-session
$ sudo apt-get update && sudo apt-get install libpcre3-dev libevent-2.1-dev
```
## Installation
### 1. From Source
@ -59,63 +36,23 @@ $ sudo apt-get update && sudo apt-get install libpcre3-dev libevent-2.1-dev
### 2. Binary Download
Alternatively, download [a pre-compiled binary
version](https://code.apps.glenux.net/glenux/mfm/releases) of MFM.
Alternatively, download a pre-compiled binary version of MFM.
## Usage
### Command Line Options
Global
```
Usage: mfm [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
Global options:
-c, --config FILE Specify configuration file
-h, --help Display this help
Commands (not implemented yet):
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
Commands:
create Add a new filesystem
delete Remove an existing filesystem
edit Modify the configuration
```
### Demo
@ -124,26 +61,24 @@ Commands (not implemented yet):
## Configuration
MFM uses a YAML configuration file, typically found at `~/.config/mfm.yml`, to
detail the filesystem names, types, and respective configurations.
MFM uses a YAML configuration file, typically found at `~/.config/mfm.yml`, to detail the filesystem names, types, and respective configurations.
### YAML File Format
```yaml
---
version: "1"
global:
mountpoint: "{{env.HOME}}/mnt"
mountpoint: "/home/user/mnt/{{name}}"
filesystems:
- type: "gocryptfs"
name: "Work - SSH Keys"
encrypted_path: "/home/user/.ssh/keyring.work.vault"
encrypted_path: "/home/user/.ssh/keyring.work"
- type: "sshfs"
name: "Personal - Media Server"
remote_user: "{{env.USER}}"
remote_user: "user"
remote_host: "mediaserver.local"
remote_path: "/mnt/largedisk/music"
remote_port: 22
@ -167,7 +102,7 @@ Contributing to MFM:
6. **Submit a Pull Request**: Begin a pull request to the main repository and explain your changes.
7. **Review**: Await feedback from the maintainers and respond as necessary.
By contributing, you agree to our code of conduct and license terms.
By contributing, you agree to our code of conduct and GPL-2 license terms.
## Authors and Contributors

2
Vagrantfile vendored
View file

@ -30,5 +30,5 @@ Vagrant.configure('2') do |config|
machine.vm.network 'forwarded_port', guest: 80, host: 1080, host_ip: '127.0.0.1'
end
config.vm.provision 'shell', path: 'scripts/vagrant-provision/base.sh'
config.vm.provision 'shell', path: 'scripts/vagrant.provision.sh'
end

View file

@ -1,8 +1,9 @@
---
version: 1
global:
mountpoint: "{{env.HOME}}/mnt"
mountpoint: "~/mnt"
filesystems:
- type: gocryptfs
@ -15,7 +16,7 @@ filesystems:
- type: sshfs
name: "Personal - Remote Media Server"
remote_user: "{{env.USER}}"
remote_user: user
remote_host: mediaserver.local
remote_port: 22
remote_path: "/remote/path/to/media"

View file

@ -1,64 +0,0 @@
#!/bin/sh -eu
# vim: set ts=2 sw=2 et:
LOCAL_PROJECT_PATH="${1-$PWD}"
TARGET_ARCH="${2-amd64}"
DOCKER_IMAGE=""
BUILD_COMMAND=" \
shards build --static --release \
&& chown 1000:1000 -R bin \
&& find bin -type f -maxdepth 1 -exec mv {} {}_${TARGET_ARCH} \; \
"
INSTALL_CRYSTAL=" \
echo '@edge http://dl-cdn.alpinelinux.org/alpine/edge/community' >>/etc/apk/repositories \
&& apk add --update --no-cache --force-overwrite \
crystal@edge \
g++ \
gc-dev \
libxml2-dev \
llvm16-dev \
llvm16-static \
make \
musl-dev \
openssl-dev \
openssl-libs-static \
pcre-dev \
shards@edge \
yaml-dev \
yaml-static \
zlib-dev \
zlib-static \
"
# setup arch
case "$TARGET_ARCH" in
amd64) DOCKER_IMAGE="alpine" ;;
arm64) DOCKER_IMAGE="multiarch/alpine:aarch64-edge" ;;
armel) DOCKER_IMAGE="multiarch/alpine:armv7-edge" ;;
# armhf) DOCKER_IMAGE="multiarch/alpine:armhf-edge" ;;
# i386) DOCKER_IMAGE="multiarch/alpine:x86-edge" ;;
mips) DOCKER_IMAGE="multiarch/alpine:mips-edge" ;;
mipsel) DOCKER_IMAGE="multiarch/alpine:mipsel-edge" ;;
powerpc) DOCKER_IMAGE="multiarch/alpine:powerpc-edge" ;;
ppc64el) DOCKER_IMAGE="multiarch/alpine:ppc64el-edge" ;;
s390x) DOCKER_IMAGE="multiarch/alpine:s390x-edge" ;;
esac
# Compile Crystal project statically for target architecture
docker pull multiarch/qemu-user-static:register
docker run \
--rm \
--privileged \
multiarch/qemu-user-static:register \
--reset
docker run \
-it \
-v "$LOCAL_PROJECT_PATH:/app" \
-w /app \
--rm \
"$DOCKER_IMAGE" \
/bin/sh -c "$INSTALL_CRYSTAL && $BUILD_COMMAND"

View file

@ -1,82 +0,0 @@
#!/bin/sh -eu
# vim: set ts=2 sw=2 et:
LOCAL_PROJECT_PATH="${1-$PWD}"
TARGET_ARCH="${2-amd64}"
DOCKER_IMAGE=""
BUILD_COMMAND=" \
shards build --static --release \
&& chown 1000:1000 -R bin \
&& find bin -type f -maxdepth 1 -exec mv {} {}_${TARGET_ARCH} \; \
"
# crystal
INSTALL_CRYSTAL=" \
sed -i -e 's/Types: deb/Types: deb deb-src/' /etc/apt/sources.list.d/debian.sources \
&& echo 'deb http://deb.debian.org/debian unstable main' > /etc/apt/sources.list.d/sid.list \
&& echo 'deb-src http://deb.debian.org/debian unstable main' >> /etc/apt/sources.list.d/sid.list \
&& apt-get update \
&& apt-get install -y \
g++ \
libxml2-dev \
llvm-dev \
make \
libssl-dev \
libpcre3-dev \
libyaml-dev \
zlib1g-dev \
dpkg-dev \
debuild \
&& apt source crystal \
&& apt build-dep crystal \
&& ls -lF \
&& debuild -b -uc -us \
"
# setup arch
case "$TARGET_ARCH" in
amd64) DOCKER_IMAGE="debian" ;;
arm64) DOCKER_IMAGE="arm64v8/debian" ;;
armel) DOCKER_IMAGE="arm32v7/debian" ;;
armhf) DOCKER_IMAGE="armhf/debian" ;;
i386) DOCKER_IMAGE="x86/debian" ;;
mips) DOCKER_IMAGE="mips/debian" ;;
mipsel) DOCKER_IMAGE="mipsel/debian" ;;
powerpc) DOCKER_IMAGE="powerpc/debian" ;;
ppc64el) DOCKER_IMAGE="ppc64el/debian" ;;
s390x) DOCKER_IMAGE="s390x/debian" ;;
esac
# Compile Crystal project statically for target architecture
docker pull multiarch/qemu-user-static
docker run \
--rm \
--privileged \
multiarch/qemu-user-static \
--reset -p yes
set -x
docker run \
-it \
-v "$LOCAL_PROJECT_PATH:/app" \
-w /app \
--rm \
--platform linux/arm64 \
"$DOCKER_IMAGE"
exit 0
set -x
docker run \
-it \
-v "$LOCAL_PROJECT_PATH:/app" \
-w /app \
--rm \
--platform linux/arm64 \
"$DOCKER_IMAGE" \
/bin/sh -c "$INSTALL_CRYSTAL && $BUILD_COMMAND"

View file

@ -1,86 +0,0 @@
#!/bin/sh -eu
# vim: set ts=2 sw=2 et:
LOCAL_PROJECT_PATH="${1-$PWD}"
TARGET_ARCH="${2-arm64}"
DOCKER_IMAGE=""
BUILD_COMMAND=" \
shards build --static --release \
&& chown 1000:1000 -R bin \
&& find bin -type f -maxdepth 1 -exec mv {} {}_${TARGET_ARCH} \; \
"
# crystal
INSTALL_CRYSTAL=" \
sed -i -e '/^deb/d' /etc/apt/sources.list \
&& sed -i -e '/jessie.updates/d' /etc/apt/sources.list \
&& sed -i -e 's/^# deb/deb/' /etc/apt/sources.list \
&& apt-get update"
cat > /dev/null <<EOF
"
&& apt-get install -y \
g\+\+ \
gcc \
curl \
autoconf \
automake \
python2 \
libxml2-dev \
llvm-dev \
make \
libssl-dev \
libpcre2-dev \
libyaml-dev \
zlib1g-dev \
"
EOF
# setup arch
case "$TARGET_ARCH" in
amd64) DOCKER_IMAGE="debian:8" ;;
arm64) DOCKER_IMAGE="arm64v8/debian:8" ;;
armel) DOCKER_IMAGE="arm32v7/debian" ;;
armhf) DOCKER_IMAGE="armhf/debian" ;;
i386) DOCKER_IMAGE="x86/debian" ;;
mips) DOCKER_IMAGE="mips/debian" ;;
mipsel) DOCKER_IMAGE="mipsel/debian" ;;
powerpc) DOCKER_IMAGE="powerpc/debian" ;;
ppc64el) DOCKER_IMAGE="ppc64el/debian" ;;
s390x) DOCKER_IMAGE="s390x/debian" ;;
esac
# Compile Crystal project statically for target architecture
docker pull multiarch/qemu-user-static
docker run \
--rm \
--privileged \
multiarch/qemu-user-static \
--reset -p yes
set -x
docker run \
-it \
-v "$LOCAL_PROJECT_PATH:/app" \
-w /app \
--rm \
--platform linux/arm64 \
"$DOCKER_IMAGE" \
/bin/sh -c "$INSTALL_CRYSTAL && bash"
exit 0
set -x
docker run \
-it \
-v "$LOCAL_PROJECT_PATH:/app" \
-w /app \
--rm \
--platform linux/arm64 \
"$DOCKER_IMAGE" \
/bin/sh -c "$INSTALL_CRYSTAL && $BUILD_COMMAND"

View file

@ -1,32 +0,0 @@
#!/bin/sh
set -e
set -u
USER="$(test -d /vagrant && echo "vagrant" || echo "debian")"
HOSTNAME="$(hostname)"
export DEBIAN_FRONTEND=noninteractive
echo "Installing required system packages"
apt-get update --allow-releaseinfo-change
apt-get install -y \
apt-transport-https \
ca-certificates \
git \
curl \
wget \
vim \
gnupg2 \
software-properties-common
# echo "Installing mfm requirements"
# apt-get install -y \
# fzf \
# sshfs \
# httpdirfs \
# libyaml-0-2 \
# libyaml-dev \
# libpcre3-dev \
# libevent-dev

View file

@ -1,22 +0,0 @@
#!/bin/sh
# install crystal
set -e
set -u
USER="$(test -d /vagrant && echo "vagrant" || echo "debian")"
HOSTNAME="$(hostname)"
export DEBIAN_FRONTEND=noninteractive
echo "Installing required system packages"
apt-get update --allow-releaseinfo-change
echo "Installing recording requirements"
apt-get install -y \
tmux \
mdp \
bat \
asciinema \
termtosvg

View file

@ -9,6 +9,26 @@ HOSTNAME="$(hostname)"
export DEBIAN_FRONTEND=noninteractive
echo "Installing required system packages"
apt-get update --allow-releaseinfo-change
apt-get install -y \
apt-transport-https \
ca-certificates \
git \
curl \
wget \
vim \
gnupg2 \
software-properties-common
echo "Installing recording requirements"
apt-get install -y \
tmux \
mdp \
bat \
asciinema \
termtosvg
echo "Installing mfm requirements"
apt-get install -y \
fzf \
@ -19,6 +39,24 @@ apt-get install -y \
libpcre3-dev \
libevent-dev
#!/bin/sh
set -e
set -u
USER="$(test -d /vagrant && echo "vagrant" || echo "debian")"
CLUSTERS_DIR=/home/$USER/clusters
# Installation de kompose
if [ ! -f /usr/local/bin/kompose ]; then
DL="$(mktemp)"
curl \
-L https://github.com/kubernetes/kompose/releases/download/v1.22.0/kompose-linux-amd64 \
-o "$DL"
chmod +x "$DL"
mv "$DL" /usr/local/bin/kompose
fi
# Installing asdf
su - "$USER" -c "git config --global advice.detachedHead false"
su - "$USER" -c "rm -rf ~/.asdf"

View file

@ -1,22 +1,6 @@
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
shellwords:
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

View file

@ -5,30 +5,29 @@
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
name: Minimalist FUSE Manager
version: 0.2.0
version: 0.1.0
targets:
mfm:
main: src/main.cr
authors:
- Glenn Y. Rolland <glenux@glenux.net>
# authors:
# - name <email@example.com>
description: |
FIXME. write description
# description: |
# Short description of gx-vault
dependencies:
crinja:
github: straight-shoota/crinja
shellwords:
github: szTheory/shellwords-crystal
version_from_shard:
github: hugopl/version_from_shard
tablo:
github: hutou/tablo
development_dependencies:
ameba:
github: crystal-ameba/ameba
# dependencies:
# pg:
# github: will/crystal-pg
# version: "~> 0.5"
# development_dependencies:
# webmock:
# github: manastech/webmock.cr
license: GPL-3

View file

@ -1 +0,0 @@
require "spec"

View file

@ -1,46 +0,0 @@
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

View file

@ -5,38 +5,104 @@
require "option_parser"
require "./config"
require "./version"
require "./parsers/root_parser"
require "./utils/breadcrumbs"
require "./utils/fzf"
require "./file_system_manager"
require "./command_factory"
require "./fzf"
module GX
class Cli
Log = ::Log.for("cli")
@config : GX::Config
@config : 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|
breadcrumbs = Utils::BreadCrumbs.new([] of String)
Parsers::RootParser.new.build(parser, breadcrumbs, @config)
parser.banner = "Usage: #{PROGRAM_NAME} [options]\n\nGlobal options"
parser.on("-c", "--config FILE", "Set configuration file") do |path|
@config.path = path
end
parser.on("-h", "--help", "Show this help") do |flag|
STDOUT.puts parser
exit(0)
end
parser.separator("\nCommands")
parser.on("create", "Create vault") do
@config.mode = Config::Mode::Add
parser.banner = "Usage: #{PROGRAM_NAME} create [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::Add
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::Edit
end
end
pparser.parse(args)
end
def run
command = CommandFactory.create_command(@config, @config.mode)
abort("ERROR: unknown command for mode #{@config.mode}") if command.nil?
def run()
@config.load_from_file
names_display = {} of String => NamedTuple(filesystem: Filesystem, ansi_name: String)
@config.filesystems.each do |filesystem|
fs_str = filesystem.type.ljust(12,' ')
result_name =
if filesystem.mounted?
"#{fs_str} #{filesystem.name} [open]"
else
"#{fs_str} #{filesystem.name}"
end
ansi_name =
if filesystem.mounted?
"#{fs_str.colorize(:dark_gray)} #{filesystem.name} [#{ "open".colorize(:green) }]"
else
"#{fs_str.colorize(:dark_gray)} #{filesystem.name}"
end
names_display[result_name] = {
filesystem: filesystem,
ansi_name: ansi_name
}
end
result_filesystem_name = Fzf.run(names_display.values.map(&.[:ansi_name]).sort)
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
STDERR.puts "Vault not found: #{selected_filesystem}.".colorize(:red)
end
command.try &.execute
end
end
end

View file

@ -1,11 +0,0 @@
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

View file

@ -1 +0,0 @@
require "./commands/*"

View file

@ -1,13 +0,0 @@
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

View file

@ -1,15 +0,0 @@
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

View file

@ -1,15 +0,0 @@
require "./abstract_command"
module GX::Commands
class GlobalCompletion < AbstractCommand
def initialize(@config : GX::Config)
end
def execute
end
def self.handles_mode
GX::Types::Mode::GlobalConfig
end
end
end

View file

@ -1,15 +0,0 @@
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

View file

@ -1,18 +0,0 @@
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

View file

@ -1,16 +0,0 @@
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

View file

@ -1,25 +0,0 @@
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

View file

@ -1,17 +0,0 @@
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

View file

@ -1,16 +0,0 @@
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

View file

@ -1,16 +0,0 @@
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

View file

@ -1,16 +0,0 @@
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

View file

@ -1,45 +0,0 @@
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

View file

@ -1,26 +0,0 @@
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

View file

@ -1,24 +0,0 @@
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

View file

@ -3,112 +3,61 @@
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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"
require "./filesystems"
module GX
class Config
Log = ::Log.for("config")
class MissingFileError < Exception
enum Mode
Add
Edit
Run
end
record NoArgs
record AddArgs, name : String, path : String
record DelArgs, name : String
# getter filesystems : Array(Models::AbstractFilesystemConfig)
getter filesystems : Array(Filesystem)
getter home_dir : String
getter root : Models::RootConfig?
property verbose : Bool
property mode : Types::Mode
property path : String?
property mode : Mode
property path : String
property args : AddArgs.class | DelArgs.class | NoArgs.class
property auto_open : Bool
# 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?
DEFAULT_CONFIG_PATH = "mfm.yml"
def initialize
raise Models::InvalidEnvironmentError.new("Home directory not found") if !ENV["HOME"]?
def initialize()
if !ENV["HOME"]?
raise "Home directory not found"
end
@home_dir = ENV["HOME"]
@verbose = false
@auto_open = false
@mode = Types::Mode::GlobalTui
@filesystems = [] of Models::AbstractFilesystemConfig
@path = nil
@mode = Mode::Run
@filesystems = [] of Filesystem
@path = File.join(@home_dir, ".config", DEFAULT_CONFIG_PATH)
@args = NoArgs
end
private def detect_config_file
possible_files = [
File.join(@home_dir, ".config", "mfm", "config.yaml"),
File.join(@home_dir, ".config", "mfm", "config.yml"),
File.join(@home_dir, ".config", "mfm.yaml"),
File.join(@home_dir, ".config", "mfm.yml"),
File.join("/etc", "mfm", "config.yaml"),
File.join("/etc", "mfm", "config.yml"),
]
possible_files.each do |file_path|
if File.exists?(file_path)
Log.info { "Configuration file found: #{file_path}" }
return file_path if File.exists?(file_path)
else
Log.debug { "Configuration file not found: #{file_path}" }
end
end
Log.error { "No configuration file found in any of the standard locations" }
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?
config_path = detect_config_file()
end
@path = config_path
@filesystems = [] of Filesystem
if !File.exists? config_path
Log.error { "File #{path} does not exist!".colorize(:red) }
if !File.exists? @path
STDERR.puts "Error: file #{@path} does not exist!".colorize(:red)
exit(1)
end
load_filesystems(@path)
end
file_data = File.read(config_path)
file_patched = Crinja.render(file_data, {"env" => ENV.to_h})
private def load_filesystems(config_path : String)
yaml_data = YAML.parse(File.read(config_path))
vaults_data = yaml_data["filesystems"].as_a
root = Models::RootConfig.from_yaml(file_patched)
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(mount_point_base_safe, selected_filesystem.mounted_name)
end
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.from_yaml(filesystem_data.to_yaml)
# @filesystems << Filesystem.new(name, encrypted_path, "#{name}.Open")
end
@root = root
end
end
end

View file

@ -1,142 +0,0 @@
# 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

9
src/filesystems.cr Normal file
View file

@ -0,0 +1,9 @@
# 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/filesystem"

View file

@ -0,0 +1,42 @@
# 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}")
puts "Filesystem #{name} is now closed.".colorize(:green)
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"

View 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 "./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

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 "./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

49
src/filesystems/sshfs.cr Normal file
View file

@ -0,0 +1,49 @@
# 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 = ""
@[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",
["#{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

View file

@ -3,8 +3,9 @@
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
module GX::Utils
module GX
class Fzf
def self.run(list : Array(String)) : String
input = IO::Memory.new
input.puts list.join("\n")
@ -28,7 +29,8 @@ module GX::Utils
exit(1)
end
result = output.to_s.strip # .split.first?
result = output.to_s.strip #.split.first?
end
end
end

View file

@ -6,31 +6,13 @@
require "yaml"
require "colorize"
require "json"
require "log"
require "./filesystems/gocryptfs"
require "./config"
require "./cli"
struct BaseFormat < Log::StaticFormatter
def run
string @entry.severity.label.downcase
string "("
source
string "): "
message
end
end
app = GX::Cli.new
app.parse_command_line(ARGV)
app.run
Log.setup do |config|
backend = Log::IOBackend.new(formatter: BaseFormat)
config.bind "*", Log::Severity::Info, backend
if ENV["LOG_LEVEL"]?
level = Log::Severity.parse(ENV["LOG_LEVEL"]) || Log::Severity::Info
config.bind "*", level, backend
end
end
cli = GX::Cli.new
cli.parse_command_line(ARGV)
cli.run

View file

@ -1,11 +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 "./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

@ -1,38 +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::Models
class InvalidFilesystemError < Exception
end
class InvalidMountpointError < Exception
end
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

@ -1,54 +0,0 @@
module GX::Models::Concerns
module Base
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
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
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
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

View file

@ -1,30 +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"
require "./abstract_filesystem_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

View file

@ -1,38 +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_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 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

View file

@ -1,38 +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_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 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

View file

@ -1,40 +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"
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

@ -1,53 +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_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"
getter options : Array(String) = [] of String
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 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",
options,
input: STDIN,
output: STDOUT,
error: STDERR
)
return process.wait
end
end
end

View file

@ -1,5 +0,0 @@
module GX::Parsers
abstract class AbstractParser
abstract def build(parser : OptionParser, ancestors : BreadCrumbs, config : Config)
end
end

View file

@ -1,26 +0,0 @@
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

View file

@ -1,38 +0,0 @@
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

View file

@ -1,73 +0,0 @@
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

View file

@ -1,7 +0,0 @@
require "option_parser"
module GX::Parsers::Options
class ConfigInitOptions
property path : String?
end
end

View file

@ -1,6 +0,0 @@
require "option_parser"
module GX::Parsers::Options
class ConfigOptions
end
end

View file

@ -1,7 +0,0 @@
require "option_parser"
module GX::Parsers::Options
class HelpOptions
property parser_snapshot : OptionParser? = nil
end
end

View file

@ -1,92 +0,0 @@
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

View file

@ -1,21 +0,0 @@
module GX::Types
enum Mode
None
GlobalVersion
GlobalHelp
GlobalCompletion
GlobalTui
GlobalConfig
GlobalMapping
ConfigInit
MappingCreate
MappingDelete
MappingEdit
MappingList
MappingMount
MappingUmount
end
end

View file

@ -1,19 +0,0 @@
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

View file

@ -1,17 +0,0 @@
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

View file

@ -1,5 +0,0 @@
require "version_from_shard"
module GX
VersionFromShard.declare
end

View file

@ -1,45 +0,0 @@
#!/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