diff --git a/.drone.yml b/.drone.yml index 0c804e0..8999d06 100644 --- a/.drone.yml +++ b/.drone.yml @@ -5,7 +5,7 @@ name: default steps: - name: build:binary - image: crystallang/crystal:1.7.3 + image: crystallang/crystal:1.10.1-alpine environment: PACKAGE_BASENAME: mfm_linux_amd64 volumes: @@ -13,19 +13,27 @@ steps: path: /_cache commands: - pwd - - apt-get update && - apt-get install -y cmake g++ libevent-dev libpcre3-dev libyaml-dev + # - | + # apt-get update && \ + # apt-get install -y \ + # cmake g++ \ + # libevent-dev libpcre3-dev \ + # libyaml-dev liblzma-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 - name: publish:tag - image: curlimages/curl + image: alpine environment: - PACKAGE_UPLOAD_URL: https://code.apps.glenux.net/api/packages/glenux/generic/mfm + PACKAGE_UPLOAD_URL: https://code.apps.glenux.net/api/v1/packages/glenux/generic/mfm + RELEASES_URL: https://code.apps.glenux.net/api/v1/repos/glenux/mfm/releases PACKAGE_BASENAME: mfm_linux_amd64 + RELEASE_UPLOAD_TOKEN: + from_secret: RELEASE_UPLOAD_TOKEN PACKAGE_UPLOAD_TOKEN: from_secret: PACKAGE_UPLOAD_TOKEN when: @@ -36,16 +44,52 @@ steps: - name: cache path: /_cache commands: + - apk add --update --no-cache curl jq - env |grep DRONE - | curl -H "Authorization: token $PACKAGE_UPLOAD_TOKEN" \ - --upload-file /_cache/bin/$PACKAGE_BASENAME \ - $PACKAGE_UPLOAD_URL/$DRONE_TAG/$PACKAGE_BASENAME + --upload-file "/_cache/bin/$PACKAGE_BASENAME" \ + "$PACKAGE_UPLOAD_URL/$DRONE_TAG/$PACKAGE_BASENAME" + - | + set -x + curl -X POST \ + -H "Authorization: token $RELEASE_UPLOAD_TOKEN" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d "{\"body\": \"DRAFT\", \"draft\": true, \"name\": \"$DRONE_TAG - DRAFT\", \"prerelease\": false, \"tag_name\": \"$DRONE_TAG\", \"target_commitish\": \"$DRONE_COMMIT_SHA\"}" \ + "$RELEASES_URL" + - | + curl -X 'GET' \ + -H 'accept: application/json' \ + "$RELEASES_URL/tags/$DRONE_TAG" + - | + TAG_ID="$(curl -X 'GET' \ + -H 'accept: application/json' \ + "$RELEASES_URL/tags/$DRONE_TAG" | jq -r .id)" + echo "TAG_ID=$TAG_ID" + - | + set -x + curl -X POST \ + -H "Authorization: token $RELEASE_UPLOAD_TOKEN" \ + -H "accept: application/json" \ + -H "Content-Type: multipart/form-data" \ + -F "attachment=@/_cache/bin/$PACKAGE_BASENAME" \ + "$RELEASES_URL/$TAG_ID/assets?name=$PACKAGE_BASENAME" + # 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: {} # diff --git a/README.md b/README.md index 15dc95a..bc58143 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ # Copyright © 2023 Glenn Y. Rolland --> +[![Build Status](https://cicd.apps.glenux.net/api/badges/glenux/mfm/status.svg)](https://cicd.apps.glenux.net/glenux/mfm) + # Minimalist Fuse Manager (MFM) MFM is a Crystal-lang CLI designed to streamline the management of various FUSE filesystems, such as sshfs, gocryptfs, httpdirfs, and more. Through its user-friendly interface, users can effortlessly mount and unmount filesystems, get real-time filesystem status, and handle errors proficiently. @@ -17,11 +19,27 @@ Before using MFM, make sure the following tools are installed on your system: - **sshfs**: - **httpdirfs**: - **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**: +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 @@ -34,7 +52,8 @@ To build from source, you'll also need: ### 2. Binary Download -Alternatively, download a pre-compiled binary version of MFM. +Alternatively, download [a pre-compiled binary +version](https://code.apps.glenux.net/glenux/mfm/releases) of MFM. ## Usage @@ -47,7 +66,7 @@ Global options: -c, --config FILE Specify configuration file -h, --help Display this help -Commands: +Commands (not implemented yet): create Add a new filesystem delete Remove an existing filesystem edit Modify the configuration @@ -59,7 +78,8 @@ Commands: ## 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 @@ -84,7 +104,7 @@ filesystems: - type: httpdirfs name: "Debian Repository" url: "http://ftp.debian.org/debian/" - + # Add more filesystems as needed ``` @@ -100,7 +120,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 GPL-2 license terms. +By contributing, you agree to our code of conduct and license terms. ## Authors and Contributors diff --git a/Vagrantfile b/Vagrantfile index d5596e1..6ea27b7 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -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.sh' + config.vm.provision 'shell', path: 'scripts/vagrant-provision/base.sh' end diff --git a/scripts/ci.crossbuild-alpine.sh b/scripts/ci.crossbuild-alpine.sh new file mode 100755 index 0000000..9a870e6 --- /dev/null +++ b/scripts/ci.crossbuild-alpine.sh @@ -0,0 +1,64 @@ +#!/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" + diff --git a/scripts/ci.crossbuild-debian.sh b/scripts/ci.crossbuild-debian.sh new file mode 100755 index 0000000..cfa6189 --- /dev/null +++ b/scripts/ci.crossbuild-debian.sh @@ -0,0 +1,82 @@ +#!/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" + diff --git a/scripts/compiler.crossbuild-debian.sh b/scripts/compiler.crossbuild-debian.sh new file mode 100755 index 0000000..54d8a6e --- /dev/null +++ b/scripts/compiler.crossbuild-debian.sh @@ -0,0 +1,86 @@ +#!/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 < 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 + + 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, @@ -93,7 +118,7 @@ module GX } end - result_filesystem_name = Fzf.run(names_display.values.map(&.[:ansi_name]).sort) + result_filesystem_name = Fzf.run(names_display.values.map(&.[:ansi_name]).sort).strip selected_filesystem = names_display[result_filesystem_name][:filesystem] puts ">> #{selected_filesystem.name}".colorize(:yellow) diff --git a/src/config.cr b/src/config.cr index 18005fc..913c066 100644 --- a/src/config.cr +++ b/src/config.cr @@ -3,14 +3,20 @@ # SPDX-FileCopyrightText: 2023 Glenn Y. Rolland # Copyright © 2023 Glenn Y. Rolland +require "crinja" + require "./filesystems" module GX class Config + Log = ::Log.for("config") + enum Mode - Add - Edit - Run + ConfigAdd + ConfigDelete + ConfigEdit + ShowVersion + Mount end record NoArgs @@ -19,36 +25,69 @@ module GX getter filesystems : Array(Filesystem) getter home_dir : String + property verbose : Bool property mode : Mode - property path : String + property path : String? property args : AddArgs.class | DelArgs.class | NoArgs.class - DEFAULT_CONFIG_PATH = "mfm.yml" - def initialize() if !ENV["HOME"]? raise "Home directory not found" end @home_dir = ENV["HOME"] - @mode = Mode::Run + @verbose = false + @mode = Mode::Mount @filesystems = [] of Filesystem - @path = File.join(@home_dir, ".config", DEFAULT_CONFIG_PATH) + @path = nil + @args = NoArgs end + 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 "Configuration file not found" + end + def load_from_file + path = @path + if path.nil? + path = detect_config_file() + end + @path = path @filesystems = [] of Filesystem - if !File.exists? @path - STDERR.puts "Error: file #{@path} does not exist!".colorize(:red) + if !File.exists? path + Log.error { "File #{path} does not exist!".colorize(:red) } exit(1) end - load_filesystems(@path) + load_filesystems(path) end private def load_filesystems(config_path : String) - yaml_data = YAML.parse(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}) + + yaml_data = YAML.parse(file_patched) vaults_data = yaml_data["filesystems"].as_a vaults_data.each do |filesystem_data| diff --git a/src/filesystems/sshfs.cr b/src/filesystems/sshfs.cr index 802639b..44ddab9 100644 --- a/src/filesystems/sshfs.cr +++ b/src/filesystems/sshfs.cr @@ -12,6 +12,7 @@ module GX 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 = "" @@ -34,7 +35,11 @@ module GX error = STDERR process = Process.new( "sshfs", - ["#{remote_user}@#{remote_host}:#{remote_path}", mount_dir], + [ + "-p", remote_port, + "#{remote_user}@#{remote_host}:#{remote_path}", + mount_dir + ], input: input, output: output, error: error diff --git a/src/main.cr b/src/main.cr index 43192b0..f32183c 100644 --- a/src/main.cr +++ b/src/main.cr @@ -6,11 +6,33 @@ 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 + +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 + + app = GX::Cli.new app.parse_command_line(ARGV) app.run