summaryrefslogtreecommitdiff
path: root/lua/rclone
diff options
context:
space:
mode:
Diffstat (limited to 'lua/rclone')
-rw-r--r--lua/rclone/health.lua19
-rw-r--r--lua/rclone/init.lua562
2 files changed, 581 insertions, 0 deletions
diff --git a/lua/rclone/health.lua b/lua/rclone/health.lua
new file mode 100644
index 0000000..5259213
--- /dev/null
+++ b/lua/rclone/health.lua
@@ -0,0 +1,19 @@
+local health = vim.health or require('health')
+
+local function check()
+ if pcall(require, 'plenary') then
+ health.report_ok('plenary installed')
+ else
+ health.report_error('nvim-lua/plenary.nvim missing')
+ end
+
+ if vim.fn.executable('rclone') == 1 then
+ health.report_ok('rclone installed')
+ else
+ health.report_error('rclone (rclone.org) not installed')
+ end
+end
+
+return {
+ check = check
+}
diff --git a/lua/rclone/init.lua b/lua/rclone/init.lua
new file mode 100644
index 0000000..df7aec6
--- /dev/null
+++ b/lua/rclone/init.lua
@@ -0,0 +1,562 @@
+local config_file = ''
+local config_dir = ''
+
+---
+-- Find config file and set global variables
+--
+-- @return string
+local function find_config_file()
+ if config_file ~= '' then
+ return config_file
+ end
+
+ local scandir = require('plenary.scandir').scan_dir
+ local Path = require('plenary.path')
+
+ local current_dir = Path:new('.')
+ while config_file == '' do
+ local files = scandir(current_dir.filename, { depth = 1 })
+ for _, file in ipairs(files) do
+ if string.find(file, 'rclone.conf') then
+ config_file = file
+ config_dir = current_dir:absolute()
+
+ return config_file
+ end
+ end
+
+ --if current_dir:is_root() then
+ if current_dir.filename == '/' then
+ -- set to 'rclone config file' output
+ local handle = io.popen('rclone config file', 'r')
+ handle:read() -- advance one line because we want the second line only
+ local output = handle:read();
+ handle:close()
+
+ config_file = output
+ local config_file_path = Path:new(config_file)
+ config_dir = config_file_path:parent().filename
+
+ return config_file
+ end
+
+ current_dir = current_dir:parent()
+ end
+end
+
+---
+-- Get rclone config as JSON
+-- and parse to table
+--
+-- @return string
+local function get_config()
+ local handle = io.popen('rclone config dump --config=' .. find_config_file(), 'r')
+ local config = handle:read('*all');
+ handle:close()
+
+ return vim.fn.json_decode(config)
+end
+
+---
+-- Get the desired remote config from the config file
+--
+-- @param remote string
+--
+-- @return string
+-- @return table
+-- @return nil
+local function get_remote_config(remote)
+ local config = get_config()
+ local target_remote = nil
+ local target_config = nil
+
+ if remote then
+ if config[remote] then
+ target_remote = remote
+ target_config = config[remote]
+ else
+ print("Couldn't find remote: \"" .. remote .. '"')
+ return
+ end
+ else
+ local config_length = 0
+ for _, _ in pairs(config) do
+ config_length = config_length + 1
+ end
+
+ if config_length == 0 then
+ print('No remotes defined in config file at "' .. find_config_file() .. '"')
+ return
+ end
+
+ if config_length == 1 then
+ for remote_name, remote_config in pairs(config) do
+ target_remote = remote_name
+ target_config = remote_config
+ end
+ else
+ for remote_name, remote_config in pairs(config) do
+ if remote_config.vim_rclone_default then
+ target_remote = remote_name
+ target_config = remote_config
+ end
+ end
+
+ if target_remote == nil then
+ print('No default remote specified')
+ return
+ end
+ end
+ end
+
+
+ if target_config.vim_rclone_local_path == nil or target_config.vim_rclone_local_path == '' or
+ target_config.vim_rclone_remote_path == nil or target_config.vim_rclone_remote_path == '' then
+ print('No local and or remote path set for remote "' .. target_remote .. '"')
+ return
+ end
+
+ return target_remote, target_config
+end
+
+---
+-- Build the local path
+--
+-- @param config table
+--
+-- @return string
+local function prepare_cmd_local_path(config)
+ if (config.vim_rclone_local_path == '.' or config.vim_rclone_local_path == './') then
+ return config_dir
+ end
+
+ local Path = require('plenary.path')
+ local local_path = Path:new(config.vim_rclone_local_path)
+
+ local_path._cwd = config_dir -- set cwd to config dir to resolve path correctly
+ local_path = local_path:absolute()
+ local_path = local_path:gsub('%./', '') -- remove wrongly kept ./ for relative paths
+
+ return local_path
+end
+
+---
+-- Add remote path
+--
+-- @param config table
+-- @param remote string
+--
+-- @return string
+local function build_cmd_remote_path(config, remote)
+ return remote .. ':' .. config.vim_rclone_remote_path
+end
+
+---
+-- Add local mount path
+--
+-- @param local_path string
+-- @param options table
+-- @param config table
+--
+-- @return string
+local function build_cmd_local_mount_path(local_path, options, config)
+ local mount_directory = options['directory'] or
+ config.vim_rclone_mount_directory or
+ vim.g.vim_rclone_mount_directory or
+ 'rclone.mount'
+
+ return local_path .. '/' .. mount_directory
+end
+
+---
+-- Add config location path
+--
+-- @return string
+local function build_cmd_config()
+ return '--config=' .. find_config_file()
+end
+
+---
+-- Add exclude patterns
+--
+-- @param local_path string
+--
+-- @return string
+local function build_cmd_exclude(local_path)
+ local Path = require('plenary.path')
+ local cmd = ''
+
+ -- add configurable default exclude patterns
+ local configurable_default_exclude_patterns = vim.g.vim_rclone_configurable_default_exclude_patterns or {'.git/', '.gitignore', '.rcloneignore'}
+ for _, pattern in pairs(configurable_default_exclude_patterns) do
+ cmd = cmd .. ' --exclude=' .. pattern
+ end
+
+ -- add .gitignore patterns
+ local gitignore = Path:new(local_path .. '/.gitignore')
+ if gitignore:exists() then
+ cmd = cmd .. ' --exclude-from=' .. gitignore.filename
+ end
+
+ local handle = io.popen('git config --get core.excludesfile', 'r')
+ local output = handle:read()
+ handle:close()
+ local gitignore_global = Path:new((Path:new(output)):expand())
+ if output ~= nil and gitignore_global:exists() then
+ cmd = cmd .. ' --exclude-from=' .. gitignore_global.filename
+ end
+
+ -- add .rcloneignore patterns
+ local rcloneignore = Path:new(local_path .. '/.rcloneignore')
+ if rcloneignore:exists() then
+ cmd = cmd .. ' --exclude-from=' .. rcloneignore.filename
+ end
+
+ return cmd:gsub('^%s?', '') -- ltrim
+end
+
+---
+-- Add logging
+--
+-- @return string
+local function build_cmd_logging()
+ local log_level = 'INFO'
+ local cmd = ''
+
+ cmd = '--log-level=' .. log_level
+ cmd = cmd .. ' --log-file=' .. vim.fn.stdpath('data') .. '/rclone_nvim/rclone.log'
+
+ return cmd
+end
+
+---
+-- Call the 'rclone copy' command with config values
+-- 'rclone copy <args>'
+--
+-- @return any
+local function copy(options)
+ local remote, config = get_remote_config(options.remote)
+ if remote == nil then
+ return
+ end
+
+ local local_path = prepare_cmd_local_path(config)
+
+ local cmd =
+ 'rclone copy ' ..
+ local_path .. ' ' ..
+ build_cmd_remote_path(config, remote) .. ' ' ..
+ build_cmd_config() .. ' ' ..
+ build_cmd_exclude(local_path) .. ' ' ..
+ build_cmd_logging()
+
+ if options['--dry-run'] then
+ print(cmd)
+ else
+ os.execute(cmd)
+ end
+
+ print("Copied!")
+
+ return config
+end
+
+---
+-- Call the 'rclone copy' command with config values for the current file
+-- 'rclone copy <args>'
+--
+-- @return any
+local function copyFile(options)
+ local remote, config = get_remote_config(options.remote)
+ if remote == nil then
+ return
+ end
+
+ local local_path = prepare_cmd_local_path(config)
+
+ -- build relative path to file for local and remote
+ local Path = require('plenary.path')
+ local local_file_path = Path:new(vim.fn.expand('%')):absolute()
+ local local_path_pattern = vim.pesc(local_path)
+
+ if local_file_path:find(local_path_pattern) == nil then
+ print('File path not in local path')
+ return
+ end
+
+ local local_file_path_relative = local_file_path:gsub(local_path_pattern, '')
+ local local_file_path_relative_parent = Path:new(local_file_path_relative):parent().filename
+
+ local cmd =
+ 'rclone copy ' ..
+ local_path .. local_file_path_relative .. ' ' ..
+ build_cmd_remote_path(config, remote) .. local_file_path_relative_parent .. ' ' ..
+ build_cmd_config() .. ' ' ..
+ build_cmd_exclude(local_path) .. ' ' ..
+ build_cmd_logging()
+
+ if options['--dry-run'] then
+ print(cmd)
+ else
+ os.execute(cmd)
+ end
+
+ print("Copied file!")
+
+ return config
+end
+
+---
+-- Call the 'rclone copy' command with config values
+-- 'rclone copy <args>'
+--
+-- @return any
+local function download(options)
+ local remote, config = get_remote_config(options.remote)
+ if remote == nil then
+ return
+ end
+
+ local local_path = prepare_cmd_local_path(config)
+
+ local cmd =
+ 'rclone copy ' ..
+ build_cmd_remote_path(config, remote) .. ' ' ..
+ local_path .. ' ' ..
+ build_cmd_config() .. ' ' ..
+ build_cmd_exclude(local_path) .. ' ' ..
+ build_cmd_logging()
+
+ if options['--dry-run'] then
+ print(cmd)
+ else
+ if vim.fn.confirm('Download?', '&Yes\n&No') == 1 then
+ os.execute(cmd)
+ vim.cmd([[
+ let curBuf=bufnr('%')
+ execute 'bufdo execute "confirm e" | update'
+ execute 'buffer ' . curBuf
+ ]])
+ else
+ print('Download canceled!')
+ return
+ end
+ end
+
+ print('Downloaded!')
+end
+
+---
+-- Call the 'rclone copy' command with config values for the current file
+-- 'rclone copy <args>'
+--
+-- @return any
+local function downloadFile(options)
+ local remote, config = get_remote_config(options.remote)
+ if remote == nil then
+ return
+ end
+
+ local local_path = prepare_cmd_local_path(config)
+
+ -- build path to file for local and remote
+ local Path = require('plenary.path')
+ local local_file_path = ''
+
+ if options['directory'] then
+ local_file_path = Path:new(vim.fn.expand(options['directory']))
+
+ if local_file_path:exists() ~= true then
+ print('Directory "' .. local_file_path:absolute() .. '" to download to missing')
+ return
+ end
+
+ local_file_path = local_file_path:absolute()
+ else
+ local_file_path = Path:new(vim.fn.expand('%')):absolute()
+ end
+
+ local local_path_pattern = vim.pesc(local_path)
+
+ if local_file_path:find(local_path_pattern) == nil then
+ print('File path not in local path')
+ return
+ end
+
+ local local_file_path_relative = local_file_path:gsub(local_path_pattern, '')
+ local local_file_path_relative_parent = Path:new(local_file_path_relative):parent().filename
+
+ if options['directory'] then
+ local_file_path_relative_parent = local_file_path_relative
+ end
+
+ local cmd =
+ 'rclone copy ' ..
+ build_cmd_remote_path(config, remote) .. local_file_path_relative .. ' ' ..
+ local_path .. local_file_path_relative_parent .. ' ' ..
+ build_cmd_config() .. ' ' ..
+ build_cmd_exclude(local_path) .. ' ' ..
+ build_cmd_logging()
+
+ if options['--dry-run'] then
+ print(cmd)
+ else
+ if vim.fn.confirm('Download file?', '&Yes\n&No') == 1 then
+ os.execute(cmd)
+ vim.api.nvim_command('confirm e')
+ else
+ print('Download file canceled!')
+ return
+ end
+ end
+
+ print('Downloaded file!')
+end
+
+---
+-- Call the 'rclone sync' command with config values
+-- 'rclone sync <args>'
+--
+-- @return any
+local function sync(options)
+ local remote, config = get_remote_config(options.remote)
+ if remote == nil then
+ return
+ end
+
+ local local_path = prepare_cmd_local_path(config)
+
+ local cmd =
+ 'rclone sync ' ..
+ local_path .. ' ' ..
+ build_cmd_remote_path(config, remote) .. ' ' ..
+ build_cmd_config() .. ' ' ..
+ build_cmd_exclude(local_path) .. ' ' ..
+ build_cmd_logging()
+
+ if options['--dry-run'] then
+ print(cmd)
+ else
+ os.execute(cmd)
+ end
+
+ print("Synced!")
+
+ return config
+end
+
+---
+-- Call the 'rclone mount' command with config values
+-- 'rclone mount <args>'
+--
+-- @return any
+local function mount(options)
+ local remote, config = get_remote_config(options.remote)
+ if remote == nil then
+ return
+ end
+
+ local local_path = prepare_cmd_local_path(config)
+ local remote_path = build_cmd_remote_path(config, remote)
+ local local_mount_path = build_cmd_local_mount_path(local_path, options, config)
+
+ local cmd =
+ 'rclone mount ' ..
+ remote_path .. ' ' ..
+ local_mount_path .. ' ' ..
+ build_cmd_config() .. ' ' ..
+ '--log-format=pid --log-file=' .. vim.fn.stdpath('data') .. '/rclone_nvim/rclone-mount.log' .. ' ' ..
+ '--read-only --no-checksum' .. ' ' ..
+ '--daemon'
+
+ if options['--dry-run'] then
+ if options['--return'] then
+ return cmd
+ end
+
+ print(cmd)
+ else
+ os.execute('mkdir -p ' .. local_mount_path)
+ os.execute(cmd)
+ end
+end
+
+---
+-- Unmount mount mounted with 'rclone mount'
+--
+-- @return any
+local function unmount(options)
+ local mount_options = vim.deepcopy(options)
+ mount_options['--dry-run'] = true
+ mount_options['--return'] = true
+ local mount_cmd = mount(mount_options)
+
+ local pid_cmd = "ps aux | grep -i '" .. mount_cmd .. "' | grep -v grep | awk '{print $2}'"
+ local cmd = function (pid)
+ return 'kill ' .. pid
+ end
+
+ if options['--dry-run'] then
+ print(pid_cmd)
+ print(cmd('$PID_RETURNED_FROM_CMD_ABOVE'))
+ else
+ local pid_handle = io.popen(pid_cmd)
+ local pid = pid_handle:read()
+ pid_handle:close()
+
+ if pid == nil then
+ print('No pid found for mount process')
+ return
+ end
+
+ os.execute(cmd(pid))
+ end
+end
+
+-- Table for dynamic command access
+local commands = {
+ copy = copy,
+ copyFile = copyFile,
+ download = download,
+ downloadFile = downloadFile,
+ sync = sync,
+ mount = mount,
+ unmount = unmount,
+}
+
+---
+-- Parse args and run specified command
+local function run(cmd, ...)
+ local args = { ... }
+ if cmd == nil then
+ print('No command supplied!')
+ return
+ end
+
+ if commands[cmd] == nil then
+ print('Command "' .. cmd .. '" does not exist.')
+ return
+ end
+
+ local options = {}
+ for _, arg in ipairs(args) do
+ if arg:find('%-%-') == 1 then
+ options[arg] = true
+ elseif arg:find('=', 1) == nil then
+ options['remote'] = arg
+ else
+ local param = vim.split(arg, '=')
+ local key = table.remove(param, 1)
+ param = table.concat(param, '=')
+
+ options[key] = param
+ end
+ end
+
+ commands[cmd](options)
+end
+
+return {
+ run = run,
+}
+