You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2530 lines
75 KiB
2530 lines
75 KiB
--- @since 25.5.31 |
|
|
|
local M = {} |
|
local SHELL = os.getenv("SHELL") or "" |
|
local HOME = os.getenv("HOME") or "" |
|
local PLUGIN_NAME = "gvfs" |
|
|
|
local USER_ID = ya.uid() |
|
local USER_NAME = tostring(ya.user_name(USER_ID)) |
|
local XDG_RUNTIME_DIR = os.getenv("XDG_RUNTIME_DIR") or ("/run/user/" .. USER_ID) |
|
|
|
local GVFS_ROOT_MOUNTPOINT = XDG_RUNTIME_DIR and (XDG_RUNTIME_DIR .. "/gvfs") or (HOME .. "/.gvfs") |
|
local GVFS_ROOT_MOUNTPOINT_FILE = "/run/media/" .. USER_NAME |
|
local SECRET_TOOL = "secret-tool" |
|
local GPG_TOOL = "gpg" |
|
local PASS_TOOL = "pass" |
|
local SECRET_VAULT_VERSION = "1" |
|
local path_separator = package.config:sub(1, 1) |
|
|
|
---@enum NOTIFY_MSG |
|
local NOTIFY_MSG = { |
|
CANT_CREATE_SAVE_FOLDER = "Can't create save folder: %s", |
|
CANT_SAVE_DEVICES = "Can't write to save file: %s", |
|
CMD_NOT_FOUND = 'Command "%s" not found. Make sure it is installed.', |
|
MOUNT_SUCCESS = 'Mounted: "%s"', |
|
MOUNT_ERROR = "Mount error: %s", |
|
CANT_REMOUNT_DEVICE = "This device can't be remounted or already mounted: %s", |
|
CANT_AUTOMOUNT = "This device can't be automounted: %s", |
|
UNMOUNT_ERROR = "Unmount error: %s", |
|
READING_GVFS_MOUNTED_FOLDER_ERROR = "Reading gvfs mounted folder error: %s", |
|
UNMOUNT_SUCCESS = 'Unmounted: "%s"', |
|
EJECT_SUCCESS = 'Ejected "%s", it can safely be removed', |
|
LIST_DEVICES_EMPTY = "No device or URI found.", |
|
REMOVED_MOUNT_URI = "Device or URI removed: %s", |
|
ADDED_MOUNT_URI = "Device or URI added: %s", |
|
UPDATED_MOUNT_URI = "Device or URI updated: %s", |
|
DEVICE_IS_DISCONNECTED = "Device or URI is disconnected", |
|
CANT_ACCESS_PREV_CWD = "Device or URI is disconnected or Previous directory is removed", |
|
URI_CANT_BE_EMPTY = "URI can't be empty", |
|
URI_IS_INVALID = "URI is invalid", |
|
UNSUPPORTED_SCHEME = "Unsupported scheme %s", |
|
UNSUPPORTED_MANUALLY_MOUNT_SCHEME = "%s scheme is mounted automatically via GNOME Online Accounts (GOA)", |
|
DISPLAY_NAME_CANT_BE_EMPTY = "Display name can't be empty", |
|
MOUNT_ERROR_PASSWORD = 'Failed to mount "%s", please check your password', |
|
MOUNT_ERROR_USERNAME = 'Failed to mount "%s", please check your username', |
|
HEADLESS_DETECTED = "GVFS.yazi plugin can only run on DBUS session. Check github HEADLESS_WORKAROUND.md to enable DBUS session", |
|
LIST_MOUNTS_EMPTY = "List mounts URI is empty", |
|
RETRIVE_PASSWORD_SUCCESS = "Retrieved password from secret vault", |
|
SAVE_PASSWORD_SUCCESS = "Saved password to secret vault", |
|
SAVE_PASSWORD_FAILED = "Save password failed: %s", |
|
SECRET_VAULT_LOCKED = "Secret vault is locked%s", |
|
PASS_INIT_GPG_ID = 'Please run "pass init <KEY_ID>" to initialize your GPG key first. \nCheck SECURE_SAVED_PASSWORD.md for the fix', |
|
MISSING_PUBLIC_KEY_GPG_KEY = "GPG key is missing public key\nCheck SECURE_SAVED_PASSWORD.md for the fix", |
|
AUTOMOUNT_WHEN_CD_STATE = "%s automount when cd for: %s", |
|
} |
|
|
|
---@enum PASSWORD_VAULT |
|
local PASSWORD_VAULT = { |
|
KEYRING = "keyring", |
|
PASS = "pass", |
|
} |
|
|
|
---@enum DEVICE_CONNECT_STATUS |
|
local DEVICE_CONNECT_STATUS = { |
|
MOUNTED = 1, |
|
NOT_MOUNTED = 2, |
|
} |
|
|
|
---@enum SCHEME |
|
local SCHEME = { |
|
MTP = "mtp", |
|
SMB = "smb", |
|
SFTP = "sftp", |
|
SSH = "ssh", -- Alias for sftp |
|
NFS = "nfs", |
|
GPHOTO2 = "gphoto2", |
|
FTP = "ftp", |
|
FTPS = "ftps", |
|
FTPIS = "ftpis", |
|
GOOGLE_DRIVE = "google-drive", |
|
ONE_DRIVE = "onedrive", |
|
DNS_SD = "dns-sd", |
|
DAV = "dav", |
|
DAVS = "davs", |
|
DAVSD = "dav+sd", |
|
DAVSSD = "davs+sd", |
|
AFP = "afp", |
|
AFC = "afc", |
|
FILE = "file", |
|
} |
|
|
|
---@enum STATE_KEY |
|
local STATE_KEY = { |
|
PREV_CWD = "PREV_CWD", |
|
WHICH_KEYS = "WHICH_KEYS", |
|
CMD_FOUND = "CMD_FOUND", |
|
DBUS_SESSION = "DBUS_SESSION", |
|
ROOT_MOUNTPOINT = "ROOT_MOUNTPOINT", |
|
SAVE_PATH = "SAVE_PATH", |
|
SAVE_PATH_AUTOMOUNTS = "SAVE_PATH_AUTOMOUNTS", |
|
MOUNTS = "MOUNTS", |
|
AUTOMOUNTS = "AUTOMOUNTS", |
|
SAVE_PASSWORD_AUTOCONFIRM = "SAVE_PASSWORD_AUTOCONFIRM", |
|
PASSWORD_VAULT = "PASSWORD_VAULT", |
|
KEY_GRIP = "KEY_GRIP", |
|
INPUT_POSITION = "INPUT_POSITION", |
|
TASKS_LOAD_GDRIVE_FOLDER = "TASKS_LOAD_GDRIVE_FOLDER", |
|
TASKS_LOAD_GDRIVE_FOLDER_RUNNING = "TASKS_LOAD_GDRIVE_FOLDER_RUNNING", |
|
BLACKLIST_DEVICES = "BLACKLIST_DEVICES", |
|
CACHED_LOCAL_PATH_DEVICE = "CACHED_LOCAL_PATH_DEVICE", |
|
} |
|
|
|
---@enum ACTION |
|
local ACTION = { |
|
SELECT_THEN_MOUNT = "select-then-mount", |
|
JUMP_TO_DEVICE = "jump-to-device", |
|
JUMP_BACK_PREV_CWD = "jump-back-prev-cwd", |
|
SELECT_THEN_UNMOUNT = "select-then-unmount", |
|
REMOUNT_KEEP_CWD_UNCHANGED = "remount-current-cwd-device", |
|
ADD_MOUNT = "add-mount", |
|
EDIT_MOUNT = "edit-mount", |
|
REMOVE_MOUNT = "remove-mount", |
|
LOAD_GDRIVE_FOLDER = "load-gdrive-folder", |
|
CACHE_LOCAL_PATH_DEVICE = "cache-local-path-device", |
|
AUTOMOUNT_WHEN_CD = "automount-when-cd", |
|
MOUNT_THEN_JUMP_SUBFOLDER = "mount-then-jump-subfolder", |
|
} |
|
|
|
---@class (exact) GdriveMountedFolderAttribute |
|
---@field display_name string |
|
---@field is_symlink "FALSE" | "TRUE" |
|
---@field name string |
|
---@field type string |
|
---@field size number |
|
---@field modified number |
|
---@field created number |
|
---@field access number |
|
---@field can_read boolean |
|
---@field can_write boolean |
|
---@field can_execute boolean |
|
---@field can_delete boolean |
|
---@field can_trash boolean |
|
---@field can_rename boolean |
|
|
|
---@class (exact) ChildrenFolderGioInfo |
|
---@field display_name string |
|
---@field uri string |
|
---@field unix_mount string |
|
---@field name string |
|
---@field type string |
|
---@field local_path string |
|
---@field attributes GdriveMountedFolderAttribute |
|
|
|
---@class (exact) Device |
|
---@field name string |
|
---@field class string? |
|
---@field mounts Mount[] |
|
---@field scheme SCHEME |
|
---@field bus integer? |
|
---@field device integer? |
|
---@field uuid string? |
|
---@field encrypted_uuid string? |
|
---@field service_domain string? |
|
---@field ["unix-device"] string? |
|
---@field owner string? |
|
---@field activation_root string? |
|
---@field uri string |
|
---@field is_manually_added boolean? |
|
---@field can_mount "1"|"0"|nil |
|
---@field can_unmount "1"|"0" |
|
---@field can_eject "1"|"0" |
|
---@field should_automount "1"|"0" |
|
---@field remote_path string? |
|
|
|
---@class (exact) Mount |
|
---@field name string |
|
---@field class string? |
|
---@field uri string |
|
---@field scheme SCHEME |
|
---@field bus integer? |
|
---@field device integer? |
|
---@field uuid string? |
|
---@field ["unix-device"] string? |
|
---@field owner string? |
|
---@field default_location string? |
|
---@field can_unmount "1"|"0"|nil |
|
---@field can_eject "1"|"0"|nil |
|
---@field is_shadowed "1"|"0"|nil |
|
|
|
-- Encode binary string to hex (e.g., "\xED" => "\\xED") |
|
local function hex_encode(s) |
|
return (s:gsub(".", function(c) |
|
return string.format("\\x%02X", c:byte()) |
|
end)) |
|
end |
|
|
|
-- Decode hex-encoded string (e.g., "\\xED" => "\xED") |
|
local function hex_decode(s) |
|
return (s:gsub("\\x(%x%x)", function(hex) |
|
return string.char(tonumber(hex, 16)) |
|
end)) |
|
end |
|
|
|
local function hex_encode_table(t) |
|
local out = {} |
|
for k, v in pairs(t) do |
|
local new_k = type(k) == "string" and hex_encode(k) or k |
|
local new_v |
|
if type(v) == "table" then |
|
new_v = hex_encode_table(v) |
|
elseif type(v) == "string" then |
|
new_v = hex_encode(v) |
|
else |
|
new_v = v |
|
end |
|
out[new_k] = new_v |
|
end |
|
return out |
|
end |
|
|
|
local function hex_decode_table(t) |
|
local out = {} |
|
for k, v in pairs(t) do |
|
local new_k = type(k) == "string" and hex_decode(k) or k |
|
local new_v |
|
if type(v) == "table" then |
|
new_v = hex_decode_table(v) |
|
elseif type(v) == "string" then |
|
new_v = hex_decode(v) |
|
else |
|
new_v = v |
|
end |
|
out[new_k] = new_v |
|
end |
|
return out |
|
end |
|
|
|
local set_state_table = ya.sync(function(state, table, key, value) |
|
if type(table) == "string" and type(key) == "string" then |
|
if not state[table] then |
|
state[table] = {} |
|
end |
|
state[table][key] = value |
|
end |
|
end) |
|
local set_state = ya.sync(function(state, key, value) |
|
state[key] = value |
|
end) |
|
|
|
local get_state = ya.sync(function(state, key) |
|
return state[key] |
|
end) |
|
|
|
local enqueue_task = ya.sync(function(state, task_name, task_data) |
|
if not state[task_name] or type(state[task_name]) ~= "table" then |
|
state[task_name] = {} |
|
end |
|
for _, _task_data in ipairs(state[task_name]) do |
|
if _task_data.folder_to_load == task_data.folder_to_load then |
|
return |
|
end |
|
end |
|
table.insert(state[task_name], task_data) |
|
end) |
|
|
|
local dequeue_task = ya.sync(function(state, task_name) |
|
if not state[task_name] or type(state[task_name]) ~= "table" then |
|
return {} |
|
end |
|
return table.remove(state[task_name]) |
|
end) |
|
|
|
---@param is_password boolean? |
|
local function show_input(title, is_password, value) |
|
local input_value, input_pw_event = ya.input({ |
|
title = title, |
|
value = value or "", |
|
obscure = is_password or false, |
|
pos = get_state(STATE_KEY.INPUT_POSITION), |
|
-- TODO: remove this after next yazi released |
|
position = get_state(STATE_KEY.INPUT_POSITION), |
|
}) |
|
if input_pw_event ~= 1 then |
|
return nil, nil |
|
end |
|
return input_value, input_pw_event |
|
end |
|
|
|
local function error(s, ...) |
|
ya.notify({ title = PLUGIN_NAME, content = string.format(s, ...), timeout = 3, level = "error" }) |
|
end |
|
|
|
local function info(s, ...) |
|
ya.notify({ title = PLUGIN_NAME, content = string.format(s, ...), timeout = 3, level = "info" }) |
|
end |
|
|
|
---run any command |
|
---@param cmd string |
|
---@param args string[] |
|
---@param _stdin? Stdio|nil |
|
---@return Error|nil, Output|nil |
|
local function run_command(cmd, args, _stdin) |
|
local stdin = _stdin or Command.INHERIT |
|
local child, cmd_err = Command(cmd) |
|
:arg(args) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:stdin(stdin) |
|
:stdout(Command.PIPED) |
|
:stderr(Command.PIPED) |
|
:spawn() |
|
|
|
if not child then |
|
error("Failed to start `%s` with error: `%s`", cmd, cmd_err) |
|
return cmd_err, nil |
|
end |
|
|
|
local output, out_err = child:wait_with_output() |
|
if not output then |
|
error("Cannot read `%s` output, error: `%s`", cmd, out_err) |
|
return out_err, nil |
|
else |
|
return nil, output |
|
end |
|
end |
|
|
|
local function is_in_dbus_session() |
|
local dbus_session = get_state(STATE_KEY.DBUS_SESSION) |
|
if dbus_session == nil then |
|
local cha, _ = fs.cha(Url(XDG_RUNTIME_DIR)) |
|
dbus_session = cha and true or false |
|
set_state(STATE_KEY.DBUS_SESSION, dbus_session) |
|
end |
|
return dbus_session |
|
end |
|
|
|
local function is_cmd_exist(cmd) |
|
local cmd_found = get_state(STATE_KEY.CMD_FOUND .. cmd) |
|
if cmd_found == nil then |
|
local _, output = run_command("which", { cmd }) |
|
cmd_found = output and output.status and output.status.success |
|
set_state(STATE_KEY.CMD_FOUND .. cmd, cmd_found) |
|
end |
|
return cmd_found |
|
end |
|
|
|
local function pathJoin(...) |
|
-- Detect OS path separator ('\' for Windows, '/' for Unix) |
|
local separator = package.config:sub(1, 1) |
|
local parts = { ... } |
|
local filteredParts = {} |
|
-- Remove empty strings or nil values |
|
for _, part in ipairs(parts) do |
|
if part and part ~= "" then |
|
table.insert(filteredParts, part) |
|
end |
|
end |
|
-- Join the remaining parts with the separator |
|
local path = table.concat(filteredParts, separator) |
|
-- Normalize any double separators (e.g., "folder//file" → "folder/file") |
|
path = path:gsub(separator .. "+", separator) |
|
|
|
return path |
|
end |
|
|
|
local function is_folder_exist(path) |
|
local err, output = run_command("[", { "-d", path, "]" }) |
|
return output and output.status and output.status.success |
|
end |
|
|
|
local function tbl_remove_empty(tbl) |
|
local cleaned = {} |
|
for _, v in pairs(tbl) do |
|
if v ~= nil and v ~= "" then |
|
table.insert(cleaned, v) |
|
end |
|
end |
|
return cleaned |
|
end |
|
|
|
local current_dir = ya.sync(function() |
|
return tostring(cx.active.current.cwd.path or cx.active.current.cwd) |
|
end) |
|
|
|
---@enum PUBSUB_KIND |
|
local PUBSUB_KIND = { |
|
cd = "cd", |
|
hover = "hover", |
|
mounts_changed = PLUGIN_NAME .. "-" .. "mounts-changed", |
|
automounts_changed = PLUGIN_NAME .. "-" .. "automounts-changed", |
|
unmounted = PLUGIN_NAME .. "-" .. "unmounted", |
|
} |
|
|
|
--- broadcast through pub sub to other instances |
|
---@param _ table state |
|
---@param pubsub_kind PUBSUB_KIND |
|
---@param data any |
|
---@param to number default = 0 to all instances |
|
local broadcast = ya.sync(function(_, pubsub_kind, data, to) |
|
ps.pub_to(to or 0, pubsub_kind, data) |
|
end) |
|
|
|
local is_dir = function(dir_path) |
|
local cha, err = fs.cha(Url(dir_path)) |
|
return not err and cha and cha.is_dir |
|
end |
|
|
|
---split string by char |
|
---@param s string |
|
---@return string[] |
|
local function string_to_array(s) |
|
local array = {} |
|
for i = 1, #s do |
|
table.insert(array, s:sub(i, i)) |
|
end |
|
return array |
|
end |
|
|
|
local function is_literal_string(str) |
|
return str and str:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") |
|
end |
|
|
|
local function tbl_deep_clone(original) |
|
if type(original) ~= "table" then |
|
return original |
|
end |
|
|
|
local copy = {} |
|
for key, value in pairs(original) do |
|
copy[tbl_deep_clone(key)] = tbl_deep_clone(value) |
|
end |
|
|
|
return copy |
|
end |
|
|
|
local function path_quote(path) |
|
if not path or path == "" then |
|
return path |
|
end |
|
local result = "'" .. string.gsub(tostring(path), "'", "'\\''") .. "'" |
|
return result |
|
end |
|
|
|
local get_hovered_path = ya.sync(function() |
|
local h = cx.active.current.hovered.path or cx.active.current.hovered |
|
if h then |
|
return tostring(h.url) |
|
end |
|
end) |
|
|
|
local function is_secret_vault_available_keyring(unlock_vault_dialog) |
|
local res, err = Command(SECRET_TOOL) |
|
:arg({ |
|
"search", |
|
PLUGIN_NAME, |
|
SECRET_VAULT_VERSION, |
|
unlock_vault_dialog and "--unlock" or nil, |
|
}) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
|
|
if err or (res and res.stderr and res.stderr:match("^secret%-tool")) then |
|
return false |
|
end |
|
return true |
|
end |
|
|
|
local function build_secret_vault_entry_gpg(protocol, user, domain, prefix, port, service_domain) |
|
protocol = protocol and ("/" .. protocol) or "" |
|
user = user and ("/" .. user) or "" |
|
domain = domain and ("/" .. domain) or "" |
|
prefix = prefix and ("/" .. prefix) or "" |
|
port = port and ("/" .. port) or "" |
|
service_domain = service_domain and ("/" .. service_domain) or "" |
|
return PLUGIN_NAME .. "/" .. SECRET_VAULT_VERSION .. protocol .. user .. domain .. port .. prefix .. service_domain |
|
end |
|
|
|
local function is_secret_vault_available_gpg(unlock_vault_dialog, is_second_run) |
|
local test_vault_entry = build_secret_vault_entry_gpg("test") |
|
local res, err = Command(SHELL) |
|
:arg({ |
|
"-c", |
|
"gpg-connect-agent 'keyinfo " .. get_state(STATE_KEY.KEY_GRIP) .. "' /bye", |
|
}) |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
|
|
if res then |
|
-- Case unlocked |
|
if res.stdout:match(".* KEYINFO [^ ]+ .+ .+ .+ 1 ") or res.stdout:match(".* KEYINFO [^ ]+ .+ .+ .+ .+ C ") then |
|
return true |
|
elseif unlock_vault_dialog and res.stdout:match(".* KEYINFO [^ ]+ .+ .+ .+ - ") then |
|
-- Display gpg unlock TUI window |
|
-- TODO: remove this after next yazi released |
|
local permit = (ui.hide or ya.hide)() |
|
-- Wrap in shell to capture exit code |
|
local full_cmd = string.format("bash -c '%s; echo __EXIT__$?__'", "pass " .. test_vault_entry .. " 2>&1") |
|
local handle = io.popen(full_cmd) |
|
local output = handle:read("*a") |
|
handle:close() |
|
|
|
-- Extract exit code |
|
local exit_code = tonumber(output:match("__EXIT__(%d+)__")) |
|
output = output:gsub("__EXIT__%d+__", ""):gsub("%s+$", "") -- clean output |
|
permit:drop() |
|
if output:match("Error: .* is not in the password store") then |
|
res, err = Command(SHELL) |
|
:arg({ |
|
"-c", |
|
("printf '%s\n%s\n' " .. path_quote("test") .. " " .. path_quote("test") .. " | ") |
|
.. PASS_TOOL |
|
.. " insert " |
|
.. " -f " |
|
.. path_quote(test_vault_entry), |
|
}) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
if res and res.stderr:match("Error: You must run:") and res.stderr:match("pass init your%-gpg%-id") then |
|
error(NOTIFY_MSG.PASS_INIT_GPG_ID) |
|
return false |
|
end |
|
if |
|
res |
|
and res.stderr:match("encryption failed: No public key") |
|
and res.stderr:match("Password encryption aborted") |
|
then |
|
error(NOTIFY_MSG.MISSING_PUBLIC_KEY_GPG_KEY) |
|
return false |
|
end |
|
|
|
if is_second_run or err or (res and res.status and not res.status.success) then |
|
return false |
|
end |
|
return is_secret_vault_available_gpg(unlock_vault_dialog, true) |
|
end |
|
return exit_code == 0 |
|
end |
|
end |
|
return false |
|
end |
|
|
|
local function is_secret_vault_available(unlock_vault_dialog) |
|
if get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.KEYRING then |
|
return is_secret_vault_available_keyring(unlock_vault_dialog) |
|
elseif get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.PASS then |
|
return is_secret_vault_available_gpg(unlock_vault_dialog) |
|
end |
|
return nil |
|
end |
|
|
|
local function save_password_keyring(password, protocol, user, domain, prefix, port, service_domain) |
|
if not user or not password or not protocol or not domain then |
|
return false |
|
end |
|
|
|
local res, err = Command(SHELL) |
|
:arg({ |
|
"-c", |
|
("printf %s " .. path_quote(password) .. " | ") |
|
.. SECRET_TOOL |
|
.. " store " |
|
.. " --label " |
|
.. path_quote( |
|
protocol |
|
.. "://" |
|
.. user |
|
.. "@" |
|
.. domain |
|
.. (port and (":" .. port) or "") |
|
.. (prefix and ("/" .. prefix) or "") |
|
.. (service_domain and ("/" .. service_domain) or "") |
|
) |
|
.. " " |
|
.. PLUGIN_NAME |
|
.. " " |
|
.. SECRET_VAULT_VERSION |
|
.. " protocol " |
|
.. protocol |
|
.. " user " |
|
.. path_quote(user) |
|
.. " domain " |
|
.. path_quote(domain) |
|
.. (port and (" port " .. port) or "") |
|
.. (prefix and (" prefix " .. path_quote(prefix)) or "") |
|
.. (service_domain and (" service_domain " .. path_quote(service_domain)) or ""), |
|
}) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
if res and res.stderr then |
|
if res.stderr:match("secret%-tool: Cannot get secret of a locked object") then |
|
error(NOTIFY_MSG.SECRET_VAULT_LOCKED) |
|
return false |
|
elseif res.stderr:match("secret%-tool: The name is not activatable") then |
|
error(NOTIFY_MSG.HEADLESS_DETECTED) |
|
return false |
|
elseif res.stderr:match("secret%-tool: Cannot autolaunch D%-Bus") then |
|
error(NOTIFY_MSG.HEADLESS_DETECTED) |
|
return false |
|
end |
|
end |
|
if err or (res and not res.status.success and res.stderr) then |
|
error(NOTIFY_MSG.SAVE_PASSWORD_FAILED, res and res.stderr or err) |
|
return false |
|
end |
|
info(NOTIFY_MSG.SAVE_PASSWORD_SUCCESS) |
|
return true |
|
end |
|
|
|
local function save_password_gpg(password, protocol, user, domain, prefix, port, service_domain) |
|
if not user or not password or not protocol or not domain then |
|
return false |
|
end |
|
|
|
local res, err = Command(SHELL) |
|
:arg({ |
|
"-c", |
|
("printf '%s\n%s\n' " .. path_quote(password) .. " " .. path_quote(password) .. " | ") |
|
.. PASS_TOOL |
|
.. " insert " |
|
.. " -f " |
|
.. path_quote(build_secret_vault_entry_gpg(protocol, user, domain, prefix, port, service_domain)), |
|
}) |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
|
|
if err or (res and res.status and not res.status.success) then |
|
error(NOTIFY_MSG.SAVE_PASSWORD_FAILED, res and res.stderr or err) |
|
return false |
|
end |
|
|
|
info(NOTIFY_MSG.SAVE_PASSWORD_SUCCESS) |
|
return true |
|
end |
|
|
|
local function save_password(password, protocol, user, domain, prefix, port, service_domain) |
|
if get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.KEYRING then |
|
return save_password_keyring(password, protocol, user, domain, prefix, port, service_domain) |
|
elseif get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.PASS then |
|
return save_password_gpg(password, protocol, user, domain, prefix, port, service_domain) |
|
end |
|
return false |
|
end |
|
|
|
local function lookup_password_keyring(protocol, user, domain, prefix, port, service_domain) |
|
if not user or not protocol or not domain then |
|
return nil |
|
end |
|
local res, err = Command(SECRET_TOOL) |
|
:arg(tbl_remove_empty({ |
|
"lookup", |
|
PLUGIN_NAME, |
|
SECRET_VAULT_VERSION, |
|
"protocol", |
|
protocol, |
|
"user", |
|
user, |
|
"domain", |
|
domain, |
|
port and "port" or nil, |
|
port and port or nil, |
|
prefix and "prefix" or nil, |
|
prefix and prefix or nil, |
|
service_domain and "service_domain" or nil, |
|
service_domain and service_domain or nil, |
|
})) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
if not err and res and res.status and res.status.success then |
|
return res.stdout |
|
end |
|
|
|
return nil |
|
end |
|
|
|
local function lookup_password_gpg(protocol, user, domain, prefix, port, service_domain) |
|
if not user or not protocol or not domain then |
|
return nil |
|
end |
|
local res, err = Command(PASS_TOOL) |
|
:arg({ |
|
build_secret_vault_entry_gpg(protocol, user, domain, prefix, port, service_domain), |
|
}) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
if not err and res and res.status and res.status.success then |
|
return res.stdout |
|
end |
|
|
|
return nil |
|
end |
|
|
|
local function lookup_password(protocol, user, domain, prefix, port, service_domain) |
|
if get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.KEYRING then |
|
return lookup_password_keyring(protocol, user, domain, prefix, port, service_domain) |
|
elseif get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.PASS then |
|
return lookup_password_gpg(protocol, user, domain, prefix, port, service_domain) |
|
end |
|
return nil |
|
end |
|
|
|
local function clear_password_keyring(protocol, user, domain, prefix, port, service_domain) |
|
local res, err = Command(SECRET_TOOL) |
|
:arg(tbl_remove_empty({ |
|
"clear", |
|
PLUGIN_NAME, |
|
SECRET_VAULT_VERSION, |
|
protocol and "protocol" or nil, |
|
protocol and protocol or nil, |
|
user and "user" or nil, |
|
user and user or nil, |
|
domain and "domain" or nil, |
|
domain and domain or nil, |
|
port and "port" or nil, |
|
port and port or nil, |
|
prefix and "prefix" or nil, |
|
prefix and prefix or nil, |
|
service_domain and "service_domain" or nil, |
|
service_domain and service_domain or nil, |
|
})) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
if res and res.stderr and not res.status.success then |
|
if res.stderr:match("secret%-tool: Cannot get secret of a locked object") then |
|
error(NOTIFY_MSG.SECRET_VAULT_LOCKED) |
|
return false |
|
elseif res.stderr:match("secret%-tool: The name is not activatable") then |
|
error(NOTIFY_MSG.HEADLESS_DETECTED) |
|
return false |
|
elseif res.stderr:match("secret%-tool: Cannot autolaunch D%-Bus") then |
|
error(NOTIFY_MSG.HEADLESS_DETECTED) |
|
return false |
|
end |
|
end |
|
|
|
if not err and res and res.status and res.status.success then |
|
return true |
|
end |
|
return false |
|
end |
|
|
|
local function clear_password_gpg(protocol, user, domain, prefix, port, service_domain) |
|
local res, err = Command(PASS_TOOL) |
|
:arg({ |
|
"rm", |
|
"-r", |
|
"-f", |
|
build_secret_vault_entry_gpg(protocol, user, domain, prefix, port, service_domain), |
|
}) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
if not err and res and res.status and res.status.success then |
|
return true |
|
end |
|
return false |
|
end |
|
|
|
local function clear_password(protocol, user, domain, prefix, port, service_domain) |
|
if get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.KEYRING then |
|
return clear_password_keyring(protocol, user, domain, prefix, port, service_domain) |
|
elseif get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.PASS then |
|
return clear_password_gpg(protocol, user, domain, prefix, port, service_domain) |
|
end |
|
return false |
|
end |
|
|
|
local function extract_info_from_uri(s) |
|
local user |
|
local domain |
|
local port |
|
local service_domain |
|
|
|
-- Attempt 1: Look for user@domain:port first (if it exists) |
|
local scheme, temp_user, temp_domain_part = s:match("^([^:]+)://([^@/]+)@([^/]+)") |
|
if temp_user and temp_domain_part then |
|
-- If user@domain found, the domain might be followed by a comma |
|
-- We want the part before the first comma or slash in the domain part |
|
user = temp_user |
|
-- domain:port |
|
domain, port = temp_domain_part:match("^([^:/]+):([^:/]+)") |
|
if not port or port == "" then |
|
port = nil |
|
domain = temp_domain_part:match("^[^/]+") or temp_domain_part |
|
end |
|
else |
|
-- Attempt 2: No user@domain, so try to get domain from the start (before first comma or slash) |
|
scheme, temp_domain_part = s:match("^([^:]+)://([^/]+)") |
|
if temp_domain_part then |
|
domain, port = temp_domain_part:match("^([^:/]+):([^:/]+)") |
|
if not port or port == "" then |
|
port = nil |
|
domain = temp_domain_part:match("^[^/]+") or temp_domain_part |
|
end |
|
end |
|
end |
|
|
|
local ssl = (s:match("^davs") or s:match("^ftps") or s:match("^ftpis") or s:match("^https")) and true or false |
|
local prefix = s:match(".*" .. (is_literal_string(domain) or "") .. (port and ":" .. port or "") .. "/(.+)$") or nil |
|
if user then |
|
local _service_domain, _user = user:match("^([^;]+);(.+)") |
|
user = _service_domain and _user or user |
|
service_domain = _service_domain and _service_domain |
|
end |
|
return scheme, domain, user, ssl, prefix, port, service_domain |
|
end |
|
|
|
local function is_mountpoint_belong_to_volume(mount, volume) |
|
return mount.is_shadowed ~= "1" |
|
and mount.scheme |
|
and mount.scheme == volume.scheme |
|
and ( |
|
(mount.uri and mount.uri == volume.uri) |
|
or (mount.uuid and mount.uuid == volume.uuid) |
|
or (mount["unix-device"] and mount["unix-device"] == volume["unix-device"]) |
|
or (mount.bus and mount.device and mount.bus == volume.bus and mount.device == volume.device) |
|
-- Case fstab with `x-gvfs-show` |
|
or (mount.name and mount.name == volume.name and mount.scheme == SCHEME.FILE) |
|
) |
|
end |
|
|
|
--- Parser for gvfs mounted gdrive folder info |
|
---@param data string |
|
---@return ChildrenFolderGioInfo[] |
|
local function parse_gdrive_mountpoint_info(data) |
|
---@type table[] |
|
local result = {} |
|
---@type table? |
|
local current_item = nil |
|
|
|
-- Iterate over each line of the input data. |
|
for line in data:gmatch("([^\r\n]+)") do |
|
-- First, try to match an indented attribute line, e.g., " standard::type: 2" |
|
local indent, attr_key, attr_value = line:match("^(%s+)[^:]+::([^:]+):%s+(.+)$") |
|
|
|
if indent and attr_key and attr_value then |
|
-- This is an attribute line. Add it to the current item's attribute sub-table. |
|
if current_item then |
|
-- Ensure the 'attributes' sub-table exists. |
|
current_item.attributes = current_item.attributes or {} |
|
|
|
-- Clean the key (e.g., "standard::type" -> "standard_type") and assign the value. |
|
local clean_attr_key = attr_key:match("^%s*(.-)%s*$"):gsub("[%s%-]", "_") |
|
if clean_attr_key == "can_read" then |
|
current_item.attributes[clean_attr_key] = attr_value == "TRUE" |
|
elseif clean_attr_key == "can_write" then |
|
current_item.attributes[clean_attr_key] = attr_value == "TRUE" |
|
elseif clean_attr_key == "can_execute" then |
|
current_item.attributes[clean_attr_key] = attr_value == "TRUE" |
|
elseif clean_attr_key == "can_delete" then |
|
current_item.attributes[clean_attr_key] = attr_value == "TRUE" |
|
elseif clean_attr_key == "can_trash" then |
|
current_item.attributes[clean_attr_key] = attr_value == "TRUE" |
|
elseif clean_attr_key == "can_rename" then |
|
current_item.attributes[clean_attr_key] = attr_value == "TRUE" |
|
else |
|
current_item.attributes[clean_attr_key] = attr_value |
|
end |
|
end |
|
else |
|
-- If it's not an attribute, try to match a top-level key-value pair. |
|
-- The value part `(.*)` allows for keys with no value, like "attributes:". |
|
local key, value = line:match("^%s*([^:]+):%s*(.*)$") |
|
|
|
if key then |
|
-- Clean the key by trimming whitespace and replacing spaces with underscores. |
|
local clean_key = key:match("^%s*(.-)%s*$"):gsub("%s", "_") |
|
|
|
-- The "display_name" key marks the beginning of a new item block. |
|
if clean_key == "display_name" then |
|
-- If we were processing a previous item, add it to the result list. |
|
if current_item then |
|
table.insert(result, current_item) |
|
end |
|
-- Start a new table for the new item. |
|
current_item = {} |
|
end |
|
|
|
-- Add the property to the current item, provided we are inside an item block |
|
-- and the key has a value. |
|
if current_item and value and value ~= "" then |
|
current_item[clean_key] = value |
|
end |
|
end |
|
end |
|
end |
|
|
|
-- After the loop, if there's a pending item, add it to the results. |
|
if current_item then |
|
table.insert(result, current_item) |
|
end |
|
|
|
return result |
|
end |
|
|
|
local function get_gdrive_children_folder_info(parent_folder) |
|
if not parent_folder then |
|
return |
|
end |
|
parent_folder = tostring(parent_folder) |
|
if |
|
parent_folder:match( |
|
"^" .. is_literal_string(get_state(STATE_KEY.ROOT_MOUNTPOINT) .. "/google-drive:host=gmail.com") |
|
) |
|
then |
|
local output, err = Command(SHELL) |
|
:arg({ |
|
"-c", |
|
"gio info -a standard::display-name,standard::name,time::modified,time::created,time::access,standard::type,standard::is-symlink,standard::size,access::can-read,access::can-write,access::can-execute,access::can-delete,access::can-delete,access::can-rename " |
|
.. path_quote(parent_folder) |
|
.. "/*", |
|
}) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:env("LC_ALL", "C") |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
if err then |
|
error(NOTIFY_MSG.READING_GVFS_MOUNTED_FOLDER_ERROR, tostring(err)) |
|
return nil |
|
end |
|
if output and output.status.code == 0 then |
|
return parse_gdrive_mountpoint_info(output.stdout) |
|
end |
|
end |
|
end |
|
|
|
--- Display real folder name using virtual files/folders |
|
---@param children_folder_info ChildrenFolderGioInfo[] |
|
local function display_virtual_children(cwd, children_folder_info) |
|
if children_folder_info == nil or #children_folder_info == 0 then |
|
return |
|
end |
|
local id = ya.id("ft") |
|
local files = {} |
|
|
|
ya.emit("update_files", { op = fs.op("part", { id = id, url = Url(cwd), files = {} }) }) |
|
|
|
for _, gdrive_mountpoint_info in ipairs(children_folder_info) do |
|
if id ~= nil and id ~= "" then |
|
-- TODO: WORKAROUND: cwd prefix `search://` can't be joined |
|
-- local url = Url(cwd):join(gdrive_mountpoint_info.display_name) |
|
local url = |
|
Url(tostring(cwd.path or cwd) .. path_separator .. tostring(gdrive_mountpoint_info.display_name)) |
|
|
|
local kind = gdrive_mountpoint_info.type == "directory" and 1 |
|
or ( |
|
gdrive_mountpoint_info.type == "regular" and 0 |
|
or (gdrive_mountpoint_info.type == "shortcut" and 4 or 16) |
|
) |
|
|
|
-- Fix for Google AI Studio using max unsigned 64-bit integer value |
|
if gdrive_mountpoint_info.attributes.access == "18446744073709551615" then |
|
gdrive_mountpoint_info.attributes.access = nil |
|
end |
|
if gdrive_mountpoint_info.attributes.modified == "18446744073709551615" then |
|
gdrive_mountpoint_info.attributes.modified = nil |
|
end |
|
if gdrive_mountpoint_info.attributes.created == "18446744073709551615" then |
|
gdrive_mountpoint_info.attributes.created = nil |
|
end |
|
table.insert( |
|
files, |
|
File({ |
|
url = url, |
|
cha = Cha({ |
|
kind = kind, |
|
mode = tonumber(kind == 1 and "40700" or "100644", 8), |
|
len = tonumber(gdrive_mountpoint_info.attributes.size or "0"), |
|
atime = gdrive_mountpoint_info.attributes.access, |
|
mtime = gdrive_mountpoint_info.attributes.modified, |
|
ctime = gdrive_mountpoint_info.attributes.created, |
|
btime = gdrive_mountpoint_info.attributes.modified, |
|
}), |
|
}) |
|
) |
|
end |
|
end |
|
|
|
ya.emit("update_files", { op = fs.op("part", { id = id, url = Url(cwd), files = files }) }) |
|
ya.emit("update_files", { op = fs.op("done", { id = id, url = Url(cwd), cha = fs.cha(Url(cwd), false) }) }) |
|
end |
|
|
|
local function parse_devices(raw_input) |
|
local volumes = {} |
|
local mounts = {} |
|
local predefined_mounts = tbl_deep_clone(get_state(STATE_KEY.MOUNTS)) or {} |
|
local blacklist_devices = get_state(STATE_KEY.BLACKLIST_DEVICES) or {} |
|
---@type Device? |
|
local current_volume = nil |
|
---@type Mount? |
|
local current_mount = nil |
|
|
|
for m = #predefined_mounts, 1, -1 do |
|
local pm = predefined_mounts[m] |
|
if pm.uri then |
|
-- replace ssh:// with sftp:// |
|
if pm.scheme == SCHEME.SSH then |
|
pm.scheme = SCHEME.SFTP |
|
pm.uri = pm.uri:gsub("^ssh://", "sftp://") |
|
end |
|
-- keep remote path, for jumping. Only scheme that doesn't support remote path |
|
if |
|
pm.scheme == SCHEME.SFTP |
|
or pm.scheme == SCHEME.FTP |
|
or pm.scheme == SCHEME.FTPS |
|
or pm.scheme == SCHEME.FTPIS |
|
or pm.scheme == SCHEME.DNS_SD |
|
or pm.scheme == SCHEME.AFC |
|
then |
|
pm.remote_path = pm.uri:match("^[%w+]+://[^/]+/(.+)$") or "" |
|
-- Remove remote path |
|
pm.uri = pm.uri:gsub("^(%a+://[^/]+).*", "%1") |
|
end |
|
end |
|
end |
|
|
|
for line in raw_input:gmatch("[^\r\n]+") do |
|
local clean_line = line:match("^%s*(.-)%s*$") |
|
|
|
-- Match volume(0) |
|
local volume_name = clean_line:match("^Volume%(%d+%):%s*(.+)$") |
|
if line:match("^Drive%(%d+%):") then |
|
current_mount = nil |
|
current_volume = nil |
|
elseif volume_name then |
|
current_mount = nil |
|
current_volume = { name = volume_name, mounts = {} } |
|
table.insert(volumes, current_volume) |
|
|
|
-- Match mount(0) |
|
elseif clean_line:match("^Mount%(%d+%):") then |
|
current_mount = nil |
|
local mount_indent, mount_name, mount_uri = line:match("^(%s*)Mount%(%d+%):%s*(.-)%s*->%s*(.+)$") |
|
if not mount_name then |
|
mount_name = clean_line:match("^Mount%(%d+%):%s*(.+)$") |
|
end |
|
|
|
if not mount_indent or #mount_indent == 0 then |
|
current_volume = nil |
|
end |
|
current_mount = { name = mount_name or "", uri = mount_uri or "" } |
|
|
|
local mount_base_uri = mount_uri:gsub("/+$", "") |
|
local mount_uri_port = mount_base_uri:match(":%d+$") |
|
for m = #predefined_mounts, 1, -1 do |
|
local predefined_mount_base_uri = predefined_mounts[m].uri:gsub("/+$", "") |
|
if |
|
predefined_mount_base_uri == mount_base_uri |
|
or ( |
|
not mount_uri_port |
|
and predefined_mount_base_uri:gsub(":%d+$", "") == mount_base_uri:gsub(":%d+$", "") |
|
) |
|
then |
|
current_mount = table.remove(predefined_mounts, m) |
|
end |
|
end |
|
|
|
if not current_mount.scheme then |
|
for _, value in pairs(SCHEME) do |
|
if mount_uri:match("^" .. is_literal_string(value) .. ":") then |
|
current_mount.scheme = value |
|
end |
|
end |
|
end |
|
|
|
-- Case mtp/gphoto2 usb bus dev |
|
if mount_uri then |
|
local protocol, bus, device = mount_uri:match("^(%w+)://%[usb:(%d+),(%d+)%]/") |
|
-- Attach to mount or volume |
|
if protocol and (protocol == SCHEME.MTP or protocol == SCHEME.GPHOTO2) and bus and device then |
|
current_mount.bus = bus |
|
current_mount.device = device |
|
end |
|
-- file:///run/media/huyhoang/6412-E4B2 |
|
local owner, label_or_uuid = mount_uri:match("^file:///run/media/(.+)/(.+)") |
|
if owner and label_or_uuid then |
|
current_mount.owner = owner |
|
current_mount.uuid = current_volume and (current_volume.uuid or current_volume["unix-device"]) |
|
or label_or_uuid |
|
current_mount["unix-device"] = current_volume and current_volume["unix-device"] |
|
end |
|
end |
|
table.insert(mounts, current_mount) |
|
|
|
-- Match key=value metadata |
|
else |
|
local key, value = clean_line:match("^(%S+)%s*=%s*(.+)$") |
|
if not key or not value then |
|
key, value = clean_line:match("^(%S+)%s*:%s*'(.-)'$") |
|
if key == "uuid" and value then |
|
current_volume.encrypted_uuid = value |
|
end |
|
end |
|
if key and value then |
|
-- Attach to mount or volume |
|
local target = current_mount or current_volume |
|
if target then |
|
if key ~= "name" or not target[key] then |
|
target[key] = value |
|
end |
|
end |
|
else |
|
local bus, device = line:match(".*:%s*'/dev/bus/usb/(%d+)/(%d+)'") |
|
-- Attach to mount or volume |
|
if bus and device then |
|
local target = current_mount or current_volume |
|
if target then |
|
target.bus = bus |
|
target.device = device |
|
end |
|
end |
|
end |
|
end |
|
end |
|
|
|
-- Remove shadowed mounts and attach mount points to volumes |
|
for i = #volumes, 1, -1 do |
|
local v = volumes[i] |
|
if v.activation_root then |
|
v.uri = v.activation_root |
|
end |
|
|
|
if not v.uuid and v.class == "device" and v["unix-device"] then |
|
v.uuid = v["unix-device"] |
|
v.scheme = SCHEME.FILE |
|
-- Attach scheme to volume |
|
elseif (v.can_mount == "0") or v.uuid and not v.uuid:match("([^:]+)://(.+)") then |
|
-- NOTE: can_mount == "0" means that the volume is mounted fstab |
|
v.scheme = SCHEME.FILE |
|
else |
|
for _, value in pairs(SCHEME) do |
|
if |
|
(v.uri and v.uri:match("^" .. is_literal_string(value) .. ":")) |
|
or (v.uuid and v.uuid:match("^" .. is_literal_string(value) .. "://")) |
|
then |
|
v.scheme = value |
|
end |
|
end |
|
end |
|
-- NOTE: Remove volumes without scheme (fstab) |
|
if volumes[i] and not v.scheme then |
|
table.remove(volumes, i) |
|
end |
|
|
|
-- Attach mount points to volume, then remove it from mounts array |
|
for j = #mounts, 1, -1 do |
|
if is_mountpoint_belong_to_volume(mounts[j], v) then |
|
table.insert(v.mounts, table.remove(mounts, j)) |
|
end |
|
end |
|
end |
|
|
|
-- Remove shadowed mounts and attach unmapped mounts to itself |
|
for _, m in ipairs(mounts) do |
|
if m.is_shadowed ~= "1" and m.uri then |
|
m.mounts = { tbl_deep_clone(m) } |
|
table.insert(volumes, m) |
|
end |
|
end |
|
|
|
for _, m in ipairs(predefined_mounts) do |
|
m.mounts = { tbl_deep_clone(m) } |
|
table.insert(volumes, m) |
|
end |
|
|
|
if #blacklist_devices > 0 then |
|
for i = #volumes, 1, -1 do |
|
local v = volumes[i] |
|
for _, bl_device in pairs(blacklist_devices) do |
|
if type(bl_device) == "string" and v.name == bl_device then |
|
table.remove(volumes, i) |
|
elseif type(bl_device) == "table" then |
|
for bl_device_prop, bl_device_value in pairs(bl_device) do |
|
if v[bl_device_prop] ~= bl_device_value then |
|
goto skip_bl_device |
|
end |
|
end |
|
table.remove(volumes, i) |
|
end |
|
::skip_bl_device:: |
|
end |
|
end |
|
end |
|
return volumes |
|
end |
|
|
|
---@param device Device |
|
---@return string|nil |
|
local function get_mounted_path(device) |
|
if not device then |
|
return nil |
|
end |
|
if device.uri or (#device.mounts > 0 and device.mounts[1].uri) then |
|
local res, err = Command(SHELL) |
|
:arg({ |
|
"-c", |
|
"gio info " |
|
.. path_quote(device.uri or (#device.mounts > 0 and device.mounts[1].uri)) |
|
.. ' | grep -E "^local path: "', |
|
}) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:env("LC_ALL", "C") |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
if err or (res and res.status and not res.status.success) then |
|
return nil |
|
end |
|
return res.stdout:match("^local path: (.+)$"):gsub("\n", "") or nil |
|
end |
|
return nil |
|
end |
|
|
|
local function can_device_umount(device) |
|
if device and device.mounts and #device.mounts > 0 then |
|
for _, mount in ipairs(device.mounts) do |
|
if mount.can_unmount == "1" or mount.can_eject == "1" then |
|
return true |
|
end |
|
end |
|
end |
|
end |
|
|
|
---@param device Device |
|
local function is_mounted(device) |
|
if can_device_umount(device) then |
|
return true |
|
end |
|
local mountpath = get_mounted_path(device) |
|
return mountpath and is_folder_exist(mountpath) |
|
end |
|
|
|
---mount device |
|
---@param opts {device: Device, username?:string, password?: string, service_domain?: string, is_pw_saved?: boolean, skipped_secret_vault?: boolean,max_retry?: integer, retries?: integer, anonymous?: boolean} |
|
---@return boolean |
|
local function mount_device(opts) |
|
local device = opts.device |
|
local max_retry = opts.max_retry or 3 |
|
local retries = opts.retries or 0 |
|
local password = opts.password |
|
local is_pw_saved = opts.is_pw_saved |
|
local skipped_secret_vault = opts.skipped_secret_vault |
|
local username = opts.username |
|
local anonymous = opts.anonymous |
|
local service_domain = opts.service_domain |
|
local error_msg = nil |
|
|
|
local auths = "" |
|
local auth_string_format = "" |
|
if password or username then |
|
if username then |
|
auths = path_quote(username) |
|
auth_string_format = auth_string_format .. "%s\n" |
|
end |
|
if service_domain then |
|
auths = auths .. " " .. path_quote(service_domain) |
|
auth_string_format = auth_string_format .. "%s\n" |
|
end |
|
if password then |
|
auths = auths .. " " .. path_quote(password) |
|
auth_string_format = auth_string_format .. "%s\n" |
|
end |
|
end |
|
|
|
local res, err = Command(SHELL) |
|
:arg({ |
|
"-c", |
|
(auth_string_format ~= "" and "printf " .. path_quote(auth_string_format) .. " " .. auths .. " | " or "") |
|
.. " gio mount " |
|
.. (anonymous and "-a " or "") |
|
.. (device.uuid and ("-d " .. device.uuid) or path_quote(device.uri)), |
|
}) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:env("LC_ALL", "C") |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
|
|
local mount_success = res and res.status and res.status.success |
|
|
|
if mount_success then |
|
info(NOTIFY_MSG.MOUNT_SUCCESS, device.name) |
|
if password and not is_pw_saved and not skipped_secret_vault and is_secret_vault_available() then |
|
local confirmed_save_password = get_state(STATE_KEY.SAVE_PASSWORD_AUTOCONFIRM) |
|
or ya.confirm({ |
|
title = ui.Line("Remember password?"):style(th.confirm.title), |
|
body = ui.Text({ |
|
ui.Line(""), |
|
ui.Line("Press Yes to save password to secret vault."):style(th.confirm.content), |
|
ui.Line(""), |
|
}) |
|
:align(ui.Align.CENTER) |
|
:wrap(ui.Wrap.YES), |
|
-- TODO: remove this after next yazi released |
|
content = ui.Text({ |
|
ui.Line(""), |
|
ui.Line("Press Yes to save password to secret vault."):style(th.confirm.content), |
|
ui.Line(""), |
|
}) |
|
:align(ui.Align.CENTER) |
|
:wrap(ui.Wrap.YES), |
|
pos = { "center", w = 70, h = 10 }, |
|
}) |
|
|
|
if confirmed_save_password then |
|
if device.uuid then |
|
-- case hard drive |
|
save_password(password, device.scheme, device.uuid, device.uuid) |
|
else |
|
local scheme, domain, user, _, prefix, port, _service_domain = extract_info_from_uri(device.uri) |
|
save_password( |
|
password, |
|
scheme, |
|
username or user, |
|
domain, |
|
prefix, |
|
port, |
|
service_domain or (username or user or ""):match("^([^;]+);") or _service_domain |
|
) |
|
end |
|
end |
|
end |
|
return true |
|
elseif res and res.status.code == 2 then |
|
if res.stderr:match(".*volume doesn’t implement mount.*") then |
|
error_msg = string.format(NOTIFY_MSG.HEADLESS_DETECTED) |
|
retries = max_retry |
|
end |
|
if res.stderr:match(".*is already mounted.*") then |
|
return true |
|
end |
|
|
|
local stdout = res.stdout |
|
if stdout:find("\nUser: \n") or stdout:find("\nUser %[.*%]: \n") then |
|
if retries < max_retry then |
|
username, _ = show_input( |
|
"Enter username " |
|
.. (device.name and ("(" .. device.name .. ")") or (device.uri and ("(" .. device.uri .. ")") or "")) |
|
.. ":", |
|
false, |
|
username or stdout:match("User %[(.*)%]:") or "" |
|
) |
|
if username == nil then |
|
return false |
|
elseif username == "" then |
|
-- Case using anonymous user |
|
anonymous = true |
|
end |
|
else |
|
error_msg = string.format( |
|
NOTIFY_MSG.MOUNT_ERROR_USERNAME, |
|
(device.name or "NO_NAME") .. " (" .. (device.scheme or "UNKNOWN_SCHEME") .. ")" |
|
) |
|
end |
|
end |
|
if |
|
(device.scheme == SCHEME.SMB or device.scheme == SCHEME.DNS_SD or device.scheme == SCHEME.DAVSD) |
|
and ( |
|
stdout:find("\nDomain: \n") |
|
or stdout:find("\nDomain %[.*%]: \n") |
|
or stdout:find("\nUser: \n") |
|
or stdout:find("\nUser %[.*%]: \n") |
|
) |
|
then |
|
if retries < max_retry then |
|
service_domain, _ = show_input( |
|
"Enter Domain " |
|
.. (device.name and ("(" .. device.name .. ")") or (device.uri and ("(" .. device.uri .. ")") or "")) |
|
.. ":", |
|
false, |
|
service_domain or stdout:match("Domain %[(.*)%]:") or "WORKGROUP" |
|
) |
|
if service_domain == nil then |
|
return false |
|
end |
|
else |
|
error_msg = string.format( |
|
NOTIFY_MSG.MOUNT_ERROR_USERNAME, |
|
(device.name or "NO_NAME") .. " (" .. device.scheme .. ")" |
|
) |
|
end |
|
end |
|
if |
|
not anonymous |
|
and ( |
|
stdout:find("Password: \n") |
|
or stdout:find("\nUser: \n") |
|
or stdout:find("\nUser %[.*%]: \n") |
|
or stdout:find("\nDomain: \n") |
|
or stdout:find("\nDomain %[.*%]: \n") |
|
) |
|
then |
|
if username ~= opts.username or (username == nil and is_pw_saved == nil) then |
|
-- Prevent showing gpg passphrase twice |
|
if not skipped_secret_vault and not is_secret_vault_available(true) then |
|
skipped_secret_vault = true |
|
end |
|
if not skipped_secret_vault then |
|
if device.uuid then |
|
-- case hard drive |
|
password = lookup_password(device.scheme, device.uuid, device.uuid) |
|
else |
|
local scheme, domain, user, _, prefix, port, _service_domain = extract_info_from_uri(device.uri) |
|
password = lookup_password( |
|
scheme, |
|
username or user, |
|
domain, |
|
prefix, |
|
port, |
|
service_domain or (username or user or ""):match("^([^;]+);") or _service_domain |
|
) |
|
end |
|
is_pw_saved = password ~= nil |
|
if is_pw_saved then |
|
info(NOTIFY_MSG.RETRIVE_PASSWORD_SUCCESS) |
|
end |
|
end |
|
end |
|
if retries < max_retry then |
|
if not is_pw_saved then |
|
password, _ = show_input( |
|
"Enter password " |
|
.. (device.name and ("(" .. device.name .. ")") or (device.uri and ("(" .. device.uri .. ")") or "")) |
|
.. ":", |
|
true |
|
) |
|
if password == nil then |
|
return false |
|
end |
|
end |
|
else |
|
error_msg = string.format( |
|
NOTIFY_MSG.MOUNT_ERROR_PASSWORD, |
|
(device.name or "NO_NAME") .. " (" .. device.scheme .. ")" |
|
) |
|
end |
|
end |
|
end |
|
-- show notification after get max retry |
|
if retries >= max_retry then |
|
error( |
|
tostring(error_msg or (res and not res.status.success and res.stderr) or err or "Error: Unknown"):gsub( |
|
"%%", |
|
"%%%%" |
|
) |
|
) |
|
return false |
|
end |
|
|
|
-- Increase retries every run |
|
retries = retries + 1 |
|
return mount_device({ |
|
device = device, |
|
retries = retries, |
|
max_retry = max_retry, |
|
password = password, |
|
is_pw_saved = is_pw_saved, |
|
skipped_secret_vault = skipped_secret_vault, |
|
username = username, |
|
service_domain = service_domain, |
|
anonymous = anonymous, |
|
}) |
|
end |
|
|
|
--- Return list of connected devices |
|
---@return Device[] |
|
local function list_gvfs_device() |
|
---@type Device[] |
|
local devices = {} |
|
local _, res = run_command("gio", { "mount", "-li" }) |
|
if res and res.status then |
|
if res.status.success then |
|
devices = parse_devices(res.stdout) |
|
end |
|
end |
|
return devices |
|
end |
|
|
|
---Return list of mounted devices |
|
---@param status DEVICE_CONNECT_STATUS |
|
---@param filter? function |
|
---@return Device[] |
|
local function list_gvfs_device_by_status(status, filter) |
|
local devices = list_gvfs_device() |
|
|
|
local devices_filtered = {} |
|
for _, d in ipairs(devices) do |
|
if filter and not filter(d) then |
|
goto continue |
|
end |
|
local mounted = is_mounted(d) |
|
|
|
if status == DEVICE_CONNECT_STATUS.MOUNTED and mounted then |
|
table.insert(devices_filtered, d) |
|
end |
|
if status == DEVICE_CONNECT_STATUS.NOT_MOUNTED and not mounted then |
|
table.insert(devices_filtered, d) |
|
end |
|
::continue:: |
|
end |
|
return devices_filtered |
|
end |
|
|
|
--- Unmount a mounted device/uri |
|
---@param device Device |
|
---@param eject boolean? eject = true if user want to safty unplug the device |
|
---@param force boolean? Ignore outstanding file operations when unmounting or ejecting |
|
---@return boolean |
|
local function unmount_gvfs(device, eject, force, max_retry, retries) |
|
if not device then |
|
return true |
|
end |
|
max_retry = max_retry or 3 |
|
retries = retries or 0 |
|
|
|
local unmount_method = "-u" |
|
if eject then |
|
unmount_method = "-e" |
|
end |
|
for _, mount in ipairs(device.mounts ~= nil and device.mounts or { device }) do |
|
local cmd_err, res = |
|
run_command("gio", tbl_remove_empty({ "mount", unmount_method, force and "-f" or nil, mount.uri })) |
|
if cmd_err or (res and not res.status.success) then |
|
if eject and res and res.stderr:find("mount doesn.*t implement .*eject.* or .*eject_with_operation.*") then |
|
return unmount_gvfs(device, false, force) |
|
end |
|
if retries >= max_retry then |
|
error(NOTIFY_MSG.UNMOUNT_ERROR, tostring(res and (res.stderr or res.stdout))) |
|
return false |
|
end |
|
return unmount_gvfs(device, eject, force, max_retry, retries + 1) |
|
end |
|
if not cmd_err and res and res.status.success then |
|
if eject then |
|
info(NOTIFY_MSG.EJECT_SUCCESS, mount.name) |
|
else |
|
info(NOTIFY_MSG.UNMOUNT_SUCCESS, mount.name) |
|
end |
|
end |
|
return true |
|
end |
|
end |
|
|
|
---show which key to select device from list |
|
---@param devices Device|Mount[] |
|
---@return number|nil |
|
local function select_device_which_key(devices) |
|
local which_keys = get_state(STATE_KEY.WHICH_KEYS) |
|
or "1234567890qwertyuiopasdfghjklzxcvbnm-=[]\\;',./!@#$%^&*()_+{}|:\"<>?" |
|
local allow_key_array = string_to_array(which_keys) |
|
local cands = {} |
|
|
|
for idx, d in ipairs(devices) do |
|
if idx > #allow_key_array then |
|
break |
|
end |
|
table.insert(cands, { |
|
on = tostring(allow_key_array[idx]), |
|
desc = (d.name or "NO_NAME") .. " (" .. (d.scheme or "UNKNOWN_SCHEME") .. ")", |
|
}) |
|
end |
|
|
|
if #cands == 0 then |
|
return |
|
end |
|
local selected_idx = ya.which({ |
|
cands = cands, |
|
}) |
|
|
|
if selected_idx and selected_idx > 0 then |
|
return selected_idx |
|
end |
|
end |
|
|
|
---@param path string |
|
---@return string? |
|
local function get_gio_uri_from_local_path(path) |
|
local root_mountpoint = get_state(STATE_KEY.ROOT_MOUNTPOINT) |
|
if |
|
not path:match("^" .. is_literal_string(root_mountpoint) .. "(.+)$") |
|
and not path:match("^" .. is_literal_string(GVFS_ROOT_MOUNTPOINT_FILE) .. "(.+)$") |
|
then |
|
return nil |
|
end |
|
local path_info, err = Command(SHELL) |
|
:arg({ |
|
"-c", |
|
"gio info " .. path_quote(path) .. ' | grep "^uri:"', |
|
}) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:env("LC_ALL", "C") |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:output() |
|
if not path_info or err or (path_info and path_info.status and not path_info.status.success) then |
|
return nil |
|
end |
|
local path_uri = path_info.stdout:match("^uri: (.+)$"):gsub("\n", "") |
|
if not path_uri then |
|
return nil |
|
end |
|
|
|
return path_uri |
|
end |
|
|
|
---@param path string |
|
---@param state_key STATE_KEY.CACHED_LOCAL_PATH_DEVICE|STATE_KEY.AUTOMOUNTS|string |
|
---@param devices Device[]? |
|
---@return Device? |
|
local function get_device_from_local_path(path, state_key, devices) |
|
local local_path = path:match("^" .. is_literal_string(get_state(STATE_KEY.ROOT_MOUNTPOINT)) .. "/[^/]+") |
|
or path:match("^" .. is_literal_string(GVFS_ROOT_MOUNTPOINT_FILE) .. "/[^/]+") |
|
-- NOTE: Get cached gio uri or get it from command "gio info" |
|
---@type Device? |
|
local cached_device = get_state(state_key)[local_path] |
|
local device_props_to_compare = { |
|
"class", |
|
"label", |
|
"scheme", |
|
"encrypted_uuid", |
|
"service_domain", |
|
"uri", |
|
"remote_path", |
|
} |
|
local mount_props_to_compare = { |
|
"class", |
|
"uri", |
|
"scheme", |
|
"uuid", |
|
"remote_path", |
|
"name", |
|
} |
|
if cached_device then |
|
if not devices then |
|
devices = list_gvfs_device() |
|
end |
|
local matched_device = nil |
|
for _, device in ipairs(devices) do |
|
matched_device = device |
|
for _, prop in ipairs(device_props_to_compare) do |
|
if cached_device[prop] ~= device[prop] then |
|
matched_device = nil |
|
goto jump_to_compare_mounts |
|
end |
|
end |
|
if matched_device then |
|
return matched_device |
|
end |
|
::jump_to_compare_mounts:: |
|
for _, mount in ipairs(device.mounts) do |
|
matched_device = device |
|
for _, prop in ipairs(mount_props_to_compare) do |
|
if cached_device[prop] ~= mount[prop] then |
|
matched_device = nil |
|
break |
|
end |
|
end |
|
end |
|
end |
|
return matched_device |
|
else |
|
local gio_uri = get_gio_uri_from_local_path(path) |
|
if not gio_uri then |
|
return nil |
|
end |
|
if not devices then |
|
devices = list_gvfs_device() |
|
end |
|
for _, device in ipairs(devices) do |
|
if device.uri and gio_uri:match("^" .. is_literal_string(device.uri) .. ".*") then |
|
return device |
|
end |
|
for _, mount in ipairs(device.mounts) do |
|
if mount.uri and gio_uri:match("^" .. is_literal_string(mount.uri) .. ".*") then |
|
return device |
|
end |
|
end |
|
end |
|
return nil |
|
end |
|
return nil |
|
end |
|
|
|
--- Jump to device mountpoint |
|
---@param device Device? |
|
local function jump_to_device_mountpoint_action(device, retry, automount) |
|
if automount then |
|
-- Trigger Automount |
|
local automount_script = HOME .. "/.config/yazi/plugins/gvfs.yazi/assets/automount.sh" |
|
run_command("chmod", { "+x", automount_script }) |
|
run_command(automount_script, {}) |
|
end |
|
if not device then |
|
local list_devices = list_gvfs_device_by_status(DEVICE_CONNECT_STATUS.MOUNTED) |
|
device = #list_devices == 1 and list_devices[1] or nil |
|
if not device then |
|
local selected_device_idx = select_device_which_key(list_devices) |
|
if not selected_device_idx then |
|
return |
|
end |
|
device = list_devices[selected_device_idx] |
|
end |
|
end |
|
if not device then |
|
info(NOTIFY_MSG.LIST_DEVICES_EMPTY) |
|
return |
|
end |
|
local mnt_path = get_mounted_path(device) |
|
if not mnt_path and not retry then |
|
-- case hard drive encrypted -> mount uuid changed |
|
local matched_devices = list_gvfs_device_by_status(DEVICE_CONNECT_STATUS.MOUNTED, function(d) |
|
return (device.uuid and (d.encrypted_uuid == device.uuid or d.uuid == device.uuid)) |
|
or (device.uri and d.uri == device.uri) |
|
end) |
|
if #matched_devices >= 1 then |
|
device = matched_devices[1] |
|
return jump_to_device_mountpoint_action(device, true, automount) |
|
end |
|
end |
|
|
|
if mnt_path then |
|
set_state(STATE_KEY.PREV_CWD, current_dir()) |
|
if device.remote_path then |
|
mnt_path = pathJoin(mnt_path, device.remote_path) |
|
end |
|
ya.emit("cd", { mnt_path, raw = true }) |
|
else |
|
error(NOTIFY_MSG.DEVICE_IS_DISCONNECTED) |
|
end |
|
end |
|
|
|
--- Jump to previous directory |
|
local function jump_to_prev_cwd_action() |
|
local prev_cwd = get_state(STATE_KEY.PREV_CWD) |
|
if not prev_cwd then |
|
return |
|
end |
|
if is_dir(prev_cwd) then |
|
set_state(STATE_KEY.PREV_CWD, current_dir()) |
|
ya.emit("cd", { prev_cwd, raw = true }) |
|
else |
|
error(NOTIFY_MSG.CANT_ACCESS_PREV_CWD) |
|
end |
|
end |
|
|
|
--- mount action |
|
---@param opts { jump: boolean?, device: Device? }? |
|
local function mount_action(opts) |
|
local selected_device |
|
-- Let user select a device if device is not specified |
|
if not opts or not opts.device then |
|
local list_devices = list_gvfs_device_by_status(DEVICE_CONNECT_STATUS.NOT_MOUNTED, function(d) |
|
return true |
|
end) |
|
-- NOTE: Automatically select the first device if there is only one device |
|
selected_device = #list_devices == 1 and list_devices[1] or nil |
|
if not selected_device then |
|
local selected_device_idx = select_device_which_key(list_devices) |
|
if not selected_device_idx then |
|
return |
|
end |
|
selected_device = list_devices[selected_device_idx] |
|
end |
|
|
|
if #list_devices == 0 then |
|
-- If every devices are mounted, then jump to the first one |
|
local root_mountpoint = get_state(STATE_KEY.ROOT_MOUNTPOINT) |
|
local list_devices_mounted = list_gvfs_device_by_status(DEVICE_CONNECT_STATUS.MOUNTED, function(d) |
|
if d.scheme == SCHEME.FILE then |
|
return ( |
|
d.mounts |
|
and #d.mounts >= 1 |
|
and d.mounts[1].uri |
|
and ( |
|
d.mounts[1].uri:match("^" .. is_literal_string("file://" .. root_mountpoint) .. "(.+)$") |
|
or d.mounts[1].uri:match( |
|
"^" .. is_literal_string("file://" .. GVFS_ROOT_MOUNTPOINT_FILE) .. "(.+)$" |
|
) |
|
) |
|
) |
|
end |
|
return true |
|
end) |
|
selected_device = #list_devices_mounted >= 1 and list_devices_mounted[1] or nil |
|
if not selected_device then |
|
info(NOTIFY_MSG.LIST_DEVICES_EMPTY) |
|
return |
|
end |
|
--NOTE: Fall-safe x-gvfs-show |
|
local status, err = Command(SHELL) |
|
:arg({ |
|
"-c", |
|
"gio info " .. path_quote( |
|
selected_device.uri or (#selected_device.mounts > 0 and selected_device.mounts[1].uri) |
|
) .. ' | grep -E "^unix mount:.*x-gvfs-show.*"', |
|
}) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:env("LC_ALL", "C") |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:status() |
|
if err then |
|
error(NOTIFY_MSG.UNMOUNT_ERROR, tostring(err)) |
|
return |
|
end |
|
if status and status.code == 0 then |
|
error(NOTIFY_MSG.UNMOUNT_ERROR, "Can't unmount device mounted through /etc/fstab") |
|
return |
|
end |
|
|
|
jump_to_device_mountpoint_action(selected_device) |
|
return true |
|
end |
|
else |
|
selected_device = opts.device |
|
end |
|
if not selected_device then |
|
return |
|
end |
|
|
|
local success = mount_device({ |
|
device = selected_device, |
|
}) |
|
|
|
if success and opts and opts.jump then |
|
jump_to_device_mountpoint_action(selected_device) |
|
end |
|
return success |
|
end |
|
|
|
local save_tab_hovered = ya.sync(function() |
|
local hovered_item_per_tab = {} |
|
for _, tab in ipairs(cx.tabs) do |
|
local is_virtual = Url(tab.current.cwd).scheme and Url(tab.current.cwd).scheme.is_virtual |
|
table.insert(hovered_item_per_tab, { |
|
id = (type(tab.id) == "number" or type(tab.id) == "string") and tab.id or tab.id.value, |
|
cwd = tostring((is_virtual and tab.current.cwd or tab.current.cwd.path) or tab.current.cwd), |
|
}) |
|
end |
|
return hovered_item_per_tab |
|
end) |
|
|
|
local redirect_unmounted_tab_to_home = ya.sync(function(_, unmounted_url, notify) |
|
if not unmounted_url or unmounted_url == "" then |
|
return |
|
end |
|
-- broadcast to other instances |
|
if notify then |
|
broadcast(PUBSUB_KIND.unmounted, hex_encode(unmounted_url)) |
|
end |
|
for _, tab in ipairs(cx.tabs) do |
|
local is_virtual = Url(tab.current.cwd).scheme and Url(tab.current.cwd).scheme.is_virtual |
|
if ((is_virtual and tab.current.cwd or tab.current.cwd.path) or tab.current.cwd):starts_with(unmounted_url) then |
|
ya.emit("cd", { |
|
HOME, |
|
tab = (type(tab.id) == "number" or type(tab.id) == "string") and tab.id or tab.id.value, |
|
raw = true, |
|
}) |
|
end |
|
end |
|
end) |
|
|
|
--- unmount action |
|
--- @param device Device? |
|
--- @param eject boolean? eject = true if user want to safty unplug the device |
|
--- @param force boolean? Ignore outstanding file operations when unmounting or ejecting |
|
local function unmount_action(device, eject, force) |
|
local selected_device |
|
if not device then |
|
local root_mountpoint = get_state(STATE_KEY.ROOT_MOUNTPOINT) |
|
|
|
local list_devices = list_gvfs_device_by_status(DEVICE_CONNECT_STATUS.MOUNTED, function(d) |
|
if d.scheme == SCHEME.FILE then |
|
return ( |
|
d.mounts |
|
and #d.mounts >= 1 |
|
and d.mounts[1].uri |
|
and ( |
|
d.mounts[1].uri:match("^" .. is_literal_string("file://" .. root_mountpoint) .. "(.+)$") |
|
or d.mounts[1].uri:match( |
|
"^" .. is_literal_string("file://" .. GVFS_ROOT_MOUNTPOINT_FILE) .. "(.+)$" |
|
) |
|
) |
|
) |
|
end |
|
return true |
|
end) |
|
-- NOTE: Automatically select the first device if there is only one device |
|
selected_device = #list_devices == 1 and list_devices[1] or nil |
|
if not selected_device then |
|
local selected_device_idx = select_device_which_key(list_devices) |
|
if not selected_device_idx then |
|
return |
|
end |
|
selected_device = list_devices[selected_device_idx] |
|
end |
|
|
|
if not selected_device and #list_devices == 0 then |
|
info(NOTIFY_MSG.LIST_DEVICES_EMPTY) |
|
return |
|
end |
|
--NOTE: Fall-safe x-gvfs-show |
|
if selected_device then |
|
local status, err = Command(SHELL) |
|
:arg({ |
|
"-c", |
|
"gio info " .. path_quote( |
|
selected_device.uri or (#selected_device.mounts > 0 and selected_device.mounts[1].uri) |
|
) .. ' | grep -E "^unix mount:.*x-gvfs-show.*"', |
|
}) |
|
:env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) |
|
:env("LC_ALL", "C") |
|
:stderr(Command.PIPED) |
|
:stdout(Command.PIPED) |
|
:status() |
|
if err then |
|
error(NOTIFY_MSG.UNMOUNT_ERROR, tostring(err)) |
|
return |
|
end |
|
if status and status.code == 0 then |
|
error(NOTIFY_MSG.UNMOUNT_ERROR, "Can't unmount device mounted through /etc/fstab") |
|
return |
|
end |
|
end |
|
end |
|
if device then |
|
selected_device = device |
|
end |
|
if not selected_device then |
|
return |
|
end |
|
|
|
local mount_path = get_mounted_path(selected_device) |
|
if selected_device.uuid and mount_path then |
|
redirect_unmounted_tab_to_home(mount_path, true) |
|
end |
|
local success = unmount_gvfs(selected_device, eject, force) |
|
if success and not selected_device.uuid and mount_path then |
|
redirect_unmounted_tab_to_home(mount_path, true) |
|
-- cd to home for all tabs within the device, and then restore the tabs location |
|
end |
|
end |
|
|
|
---@param state_key STATE_KEY.CACHED_LOCAL_PATH_DEVICE|STATE_KEY.AUTOMOUNTS|string |
|
---@param jump_location string? |
|
---@param tab_id number? |
|
local function remount_keep_cwd_unchanged_action(state_key, jump_location, tab_id, hide_cant_remount_message) |
|
local cwd = jump_location or current_dir() |
|
local root_mountpoint = get_state(STATE_KEY.ROOT_MOUNTPOINT) |
|
if |
|
not cwd:match("^" .. is_literal_string(root_mountpoint) .. "(.+)$") |
|
and not cwd:match("^" .. is_literal_string(GVFS_ROOT_MOUNTPOINT_FILE) .. "(.+)$") |
|
then |
|
return nil |
|
end |
|
|
|
local devices = list_gvfs_device() |
|
local current_tab_device = get_device_from_local_path(cwd, state_key, devices) |
|
if not current_tab_device then |
|
info(NOTIFY_MSG.DEVICE_IS_DISCONNECTED) |
|
return |
|
end |
|
if is_mounted(current_tab_device) then |
|
if not hide_cant_remount_message then |
|
info(NOTIFY_MSG.CANT_REMOUNT_DEVICE, current_tab_device.name) |
|
end |
|
return current_tab_device |
|
end |
|
local tabs = save_tab_hovered() |
|
local saved_matched_tabs = {} |
|
-- cd to home for all tabs within the device, and then restore the tabs location |
|
if state_key == STATE_KEY.CACHED_LOCAL_PATH_DEVICE then |
|
for _, tab in ipairs(tabs) do |
|
local tab_device = get_device_from_local_path(tostring(tab.cwd), state_key, devices) |
|
if tab_device and tab_device.name == current_tab_device.name then |
|
table.insert(saved_matched_tabs, tab) |
|
ya.emit("cd", { |
|
root_mountpoint, |
|
tab = tab.id, |
|
raw = true, |
|
}) |
|
end |
|
end |
|
end |
|
mount_action({ jump = false, device = current_tab_device }) |
|
if state_key == STATE_KEY.CACHED_LOCAL_PATH_DEVICE then |
|
for _, tab in ipairs(saved_matched_tabs) do |
|
ya.emit("cd", { |
|
tostring(tab.cwd), |
|
tab = tab.id, |
|
raw = true, |
|
}) |
|
end |
|
else |
|
if jump_location then |
|
local jump_location_cha, _ = fs.cha(Url(jump_location)) |
|
ya.emit((jump_location_cha and jump_location_cha.is_dir) and "cd" or "reveal", { |
|
tostring(jump_location), |
|
no_dummy = true, |
|
raw = true, |
|
tab = tab_id, |
|
}) |
|
end |
|
end |
|
return current_tab_device |
|
end |
|
|
|
local save_automount_devices = function() |
|
local automounts = get_state(STATE_KEY.AUTOMOUNTS) |
|
|
|
local save_path = Url(get_state(STATE_KEY.SAVE_PATH_AUTOMOUNTS)) |
|
-- create parent directories |
|
local save_path_created, err_create = fs.create("dir_all", save_path.parent) |
|
|
|
if err_create then |
|
error(NOTIFY_MSG.CANT_CREATE_SAVE_FOLDER, tostring(save_path.parent)) |
|
end |
|
|
|
-- save mounts to file |
|
if save_path_created then |
|
local _, err_write = fs.write(save_path, ya.json_encode(hex_encode_table(automounts))) |
|
if err_write then |
|
error(NOTIFY_MSG.CANT_SAVE_DEVICES, tostring(save_path)) |
|
end |
|
end |
|
|
|
-- trigger update to other instances |
|
broadcast(PUBSUB_KIND.automounts_changed, hex_encode_table(automounts)) |
|
end |
|
|
|
local save_mounts = function() |
|
local mounts = get_state(STATE_KEY.MOUNTS) |
|
local mounts_to_save = {} |
|
for idx = #mounts, 1, -1 do |
|
if mounts[idx].is_manually_added then |
|
-- save name, uri, scheme, is_manually_added |
|
table.insert(mounts_to_save, 1, { |
|
name = mounts[idx].name, |
|
uri = mounts[idx].uri, |
|
scheme = mounts[idx].scheme, |
|
is_manually_added = mounts[idx].is_manually_added, |
|
}) |
|
end |
|
end |
|
|
|
local save_path = Url(get_state(STATE_KEY.SAVE_PATH)) |
|
-- create parent directories |
|
local save_path_created, err_create = fs.create("dir_all", save_path.parent) |
|
|
|
if err_create then |
|
error(NOTIFY_MSG.CANT_CREATE_SAVE_FOLDER, tostring(save_path.parent)) |
|
end |
|
|
|
-- save mounts to file |
|
if save_path_created then |
|
local _, err_write = fs.write(save_path, ya.json_encode(hex_encode_table(mounts))) |
|
if err_write then |
|
error(NOTIFY_MSG.CANT_SAVE_DEVICES, tostring(save_path)) |
|
end |
|
end |
|
|
|
-- trigger update to other instances |
|
broadcast(PUBSUB_KIND.mounts_changed, hex_encode_table(mounts)) |
|
end |
|
|
|
local read_hex_decoded_file_content = function(save_path) |
|
local file = io.open(save_path, "r") |
|
if file == nil then |
|
return {} |
|
end |
|
local encoded_data = file:read("*all") |
|
file:close() |
|
return hex_decode_table(ya.json_decode(encoded_data)) |
|
end |
|
|
|
---@param is_edit boolean? |
|
local function add_or_edit_mount_action(is_edit) |
|
---@type any |
|
local mount = { |
|
is_manually_added = true, |
|
} |
|
|
|
local selected_idx = nil |
|
|
|
if is_edit then |
|
local mounts = get_state(STATE_KEY.MOUNTS) |
|
if #mounts == 0 then |
|
info(NOTIFY_MSG.LIST_MOUNTS_EMPTY) |
|
return |
|
end |
|
selected_idx = select_device_which_key(mounts) |
|
if not selected_idx then |
|
return |
|
end |
|
mount = tbl_deep_clone(mounts[selected_idx]) |
|
end |
|
|
|
mount.uri, _ = show_input("Enter mount URI:", false, mount.uri) |
|
if mount.uri == nil then |
|
return |
|
end |
|
mount.uri = mount.uri:gsub("^%s*(.-)%s*$", "%1") |
|
if mount.uri == nil then |
|
return |
|
elseif mount.uri == "" then |
|
error(NOTIFY_MSG.URI_CANT_BE_EMPTY) |
|
end |
|
mount.uri = mount.uri:gsub("/$", "") |
|
-- sftp://test@192.168.1.2 |
|
-- ftp://huyhoang@192.168.1.2:9999/ |
|
local _scheme, uri = string.match(mount.uri, "([^:]+)://(.+)") |
|
local scheme |
|
if not _scheme or not uri then |
|
error(NOTIFY_MSG.URI_IS_INVALID) |
|
return |
|
end |
|
for _, value in pairs(SCHEME) do |
|
if _scheme == value and value ~= SCHEME.FILE then |
|
scheme = value |
|
end |
|
end |
|
|
|
mount.scheme = scheme |
|
if not scheme then |
|
error(NOTIFY_MSG.UNSUPPORTED_SCHEME, tostring(_scheme)) |
|
return |
|
end |
|
if scheme == SCHEME.GOOGLE_DRIVE or scheme == SCHEME.ONE_DRIVE then |
|
error(NOTIFY_MSG.UNSUPPORTED_MANUALLY_MOUNT_SCHEME, tostring(_scheme)) |
|
return |
|
end |
|
if scheme == SCHEME.SMB then |
|
mount.service_domain = mount.uri:match("^smb://([^;]+);") |
|
if not mount.service_domain then |
|
mount.service_domain, _ = show_input("Enter SMB domain:", false, "WORKGROUP") |
|
end |
|
if not mount.service_domain then |
|
return |
|
end |
|
end |
|
mount.name, _ = show_input("Enter display name:", false, mount.name or uri) |
|
|
|
if mount.name == nil then |
|
return |
|
end |
|
|
|
if mount.name == "" or not mount.name then |
|
error(NOTIFY_MSG.DISPLAY_NAME_CANT_BE_EMPTY) |
|
return |
|
end |
|
|
|
local mounts = get_state(STATE_KEY.MOUNTS) |
|
if selected_idx then |
|
if is_mounted(mounts[selected_idx]) then |
|
unmount_action(mounts[selected_idx], false, true) |
|
end |
|
if mount.uri ~= mounts[selected_idx].uri then |
|
local old_scheme, old_domain, old_user, _, old_prefix, old_port, old_service_domain = |
|
extract_info_from_uri(mounts[selected_idx].uri) |
|
if old_domain and old_scheme and is_secret_vault_available(true) then |
|
clear_password( |
|
old_scheme, |
|
old_user, |
|
old_domain, |
|
old_prefix, |
|
old_port, |
|
old_service_domain or mounts[selected_idx].service_domain |
|
) |
|
end |
|
end |
|
mounts[selected_idx] = mount |
|
info(NOTIFY_MSG.UPDATED_MOUNT_URI, mount.name) |
|
else |
|
table.insert(mounts, mount) |
|
info(NOTIFY_MSG.ADDED_MOUNT_URI, mount.name) |
|
end |
|
set_state(STATE_KEY.MOUNTS, mounts) |
|
save_mounts() |
|
end |
|
|
|
local function load_gdrive_folder_action() |
|
if |
|
get_state(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER_RUNNING) |
|
or not get_state(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER) |
|
or #get_state(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER) == 0 |
|
then |
|
return |
|
end |
|
set_state(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER_RUNNING, true) |
|
local data = dequeue_task(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER) |
|
local folder_to_load_raw = data.folder_to_load |
|
local self_load = data.self_load |
|
local folder_to_load = Url(folder_to_load_raw) |
|
|
|
local gdrive_mountpoint_children_info = get_gdrive_children_folder_info(folder_to_load_raw) |
|
if gdrive_mountpoint_children_info then |
|
display_virtual_children(folder_to_load, gdrive_mountpoint_children_info) |
|
end |
|
-- TODO: parent |
|
if self_load then |
|
-- ya.sleep(0.5) |
|
-- if folder_to_load.parent and folder_to_load.parent.parent then |
|
-- local gdrive_children_info_parents = get_gdrive_children_folder_info(folder_to_load.parent.parent) |
|
-- if gdrive_children_info_parents then |
|
-- display_virtual_children(folder_to_load.parent.parent, gdrive_children_info_parents) |
|
-- end |
|
-- end |
|
-- TODO: Load preview |
|
-- ya.sleep(0.5) |
|
-- local hovered_folder_cwd = current_hovered_folder_cwd() |
|
-- if hovered_folder_cwd then |
|
-- local gdrive_children_info_parents = get_gdrive_children_folder_info(hovered_folder_cwd) |
|
-- if gdrive_children_info_parents then |
|
-- display_virtual_children(hovered_folder_cwd, gdrive_children_info_parents) |
|
-- end |
|
-- end |
|
end |
|
-- ya.sleep(0.5) |
|
set_state(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER_RUNNING, false) |
|
load_gdrive_folder_action() |
|
end |
|
|
|
local function cache_local_path_device_action(local_path) |
|
local device_matched = get_device_from_local_path(local_path, STATE_KEY.CACHED_LOCAL_PATH_DEVICE) |
|
if device_matched then |
|
set_state_table(STATE_KEY.CACHED_LOCAL_PATH_DEVICE, local_path, device_matched) |
|
end |
|
end |
|
|
|
---@param enabled boolean? |
|
local function toggle_automount_when_cd_action(enabled) |
|
local hovered_path = get_hovered_path() |
|
local is_virtual = Url(hovered_path).scheme and Url(hovered_path).scheme.is_virtual |
|
if is_virtual then |
|
return |
|
end |
|
local local_path = hovered_path:match("^" .. is_literal_string(get_state(STATE_KEY.ROOT_MOUNTPOINT)) .. "/[^/]+") |
|
or hovered_path:match("^" .. is_literal_string(GVFS_ROOT_MOUNTPOINT_FILE) .. "/[^/]+") |
|
if local_path then |
|
if not enabled then |
|
set_state_table(STATE_KEY.AUTOMOUNTS, local_path, nil) |
|
else |
|
local device_matched = get_device_from_local_path(local_path, STATE_KEY.AUTOMOUNTS) |
|
if device_matched then |
|
if device_matched.mounts and #device_matched.mounts > 0 and not can_device_umount(device_matched) then |
|
info(NOTIFY_MSG.CANT_AUTOMOUNT, device_matched.name) |
|
return |
|
end |
|
-- |
|
set_state_table(STATE_KEY.AUTOMOUNTS, local_path, device_matched) |
|
end |
|
end |
|
info(NOTIFY_MSG.AUTOMOUNT_WHEN_CD_STATE, enabled and "Enabled" or "Disabled", tostring(local_path)) |
|
save_automount_devices() |
|
end |
|
end |
|
local function remove_mount_action() |
|
local mounts = get_state(STATE_KEY.MOUNTS) |
|
if #mounts == 0 then |
|
info(NOTIFY_MSG.LIST_MOUNTS_EMPTY) |
|
return |
|
end |
|
|
|
local selected_idx = select_device_which_key(mounts) |
|
if not selected_idx then |
|
return |
|
end |
|
|
|
local mount = mounts[selected_idx] |
|
if not mount then |
|
return |
|
end |
|
|
|
if is_mounted(mount) then |
|
unmount_action(mount, false, true) |
|
end |
|
-- run_command("gio", { "mount", "-u", mount.uri }) |
|
local old_scheme, old_domain, old_user, _, old_prefix, old_port, old_service_domain = |
|
extract_info_from_uri(mounts[selected_idx].uri) |
|
if old_domain and old_scheme and is_secret_vault_available(true) then |
|
clear_password( |
|
old_scheme, |
|
old_user, |
|
old_domain, |
|
old_prefix, |
|
old_port, |
|
old_service_domain or mount.service_domain |
|
) |
|
end |
|
local removed_mount = table.remove(mounts, selected_idx) |
|
set_state(STATE_KEY.MOUNTS, mounts) |
|
info(NOTIFY_MSG.REMOVED_MOUNT_URI, removed_mount.name) |
|
save_mounts() |
|
end |
|
|
|
---setup function in yazi/init.lua |
|
---@param opts {} |
|
function M:setup(opts) |
|
local st = self |
|
|
|
if opts and opts.key_grip then |
|
st[STATE_KEY.KEY_GRIP] = opts.key_grip |
|
end |
|
st[STATE_KEY.INPUT_POSITION] = (opts and type(opts.input_position) == "table") and opts.input_position |
|
or { "top-center", y = 3, w = 60 } |
|
|
|
if opts and opts.save_password_autoconfirm == true then |
|
st[STATE_KEY.SAVE_PASSWORD_AUTOCONFIRM] = true |
|
end |
|
if opts and opts.password_vault then |
|
st[STATE_KEY.PASSWORD_VAULT] = ( |
|
opts and (opts.password_vault == PASSWORD_VAULT.KEYRING or opts.password_vault == PASSWORD_VAULT.PASS) |
|
) and opts.password_vault |
|
else |
|
-- TODO: REMOVE: backwards compatibility |
|
if opts and opts.enabled_keyring == true then |
|
st[STATE_KEY.PASSWORD_VAULT] = PASSWORD_VAULT.KEYRING |
|
end |
|
end |
|
|
|
if opts and opts.which_keys and type(opts.which_keys) == "string" then |
|
st[STATE_KEY.WHICH_KEYS] = opts.which_keys |
|
end |
|
local save_path = os.getenv("HOME") .. "/.config/yazi/gvfs.private" |
|
if type(opts) == "table" then |
|
save_path = opts.save_path or save_path |
|
end |
|
|
|
local save_path_automounts = os.getenv("HOME") .. "/.config/yazi/gvfs_automounts.private" |
|
if type(opts) == "table" then |
|
save_path_automounts = opts.save_path_automounts or save_path_automounts |
|
end |
|
st[STATE_KEY.SAVE_PATH] = save_path |
|
st[STATE_KEY.SAVE_PATH_AUTOMOUNTS] = save_path_automounts |
|
|
|
-- NOTE: Use pathJoin to avoid double slashes and end with a slash |
|
if opts and opts.root_mountpoint and type(opts.root_mountpoint) == "string" then |
|
st[STATE_KEY.ROOT_MOUNTPOINT] = pathJoin(opts.root_mountpoint) |
|
else |
|
st[STATE_KEY.ROOT_MOUNTPOINT] = pathJoin(GVFS_ROOT_MOUNTPOINT) |
|
end |
|
st[STATE_KEY.BLACKLIST_DEVICES] = (opts and opts.blacklist_devices and type(opts.blacklist_devices) == "table") |
|
and opts.blacklist_devices |
|
or {} |
|
|
|
st[STATE_KEY.MOUNTS] = read_hex_decoded_file_content(get_state(STATE_KEY.SAVE_PATH)) |
|
st[STATE_KEY.AUTOMOUNTS] = read_hex_decoded_file_content(get_state(STATE_KEY.SAVE_PATH_AUTOMOUNTS)) or {} |
|
|
|
st[STATE_KEY.CACHED_LOCAL_PATH_DEVICE] = {} |
|
|
|
ps.sub(PUBSUB_KIND.cd, function(payload) |
|
local cwd = cx.active.current.cwd |
|
if not cwd then |
|
return |
|
end |
|
|
|
local cwd_raw = tostring(cwd) |
|
if |
|
cwd_raw:match( |
|
"^" .. is_literal_string(get_state(STATE_KEY.ROOT_MOUNTPOINT) .. "/google-drive:host=gmail.com") |
|
) |
|
then |
|
enqueue_task(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER, { folder_to_load = cwd_raw, self_load = true }) |
|
local args = ya.quote(ACTION.LOAD_GDRIVE_FOLDER) |
|
ya.emit("plugin", { |
|
self._id, |
|
args, |
|
}) |
|
end |
|
local local_path = cwd_raw:match("^" .. is_literal_string(st[STATE_KEY.ROOT_MOUNTPOINT]) .. "/[^/]+") |
|
or cwd_raw:match("^" .. is_literal_string(GVFS_ROOT_MOUNTPOINT_FILE) .. "/[^/]+") |
|
|
|
if local_path then |
|
if not st[STATE_KEY.CACHED_LOCAL_PATH_DEVICE][local_path] then |
|
local args = ya.quote(ACTION.CACHE_LOCAL_PATH_DEVICE) .. " " .. ya.quote(local_path) |
|
ya.emit("plugin", { |
|
self._id, |
|
args, |
|
}) |
|
end |
|
if st[STATE_KEY.AUTOMOUNTS][local_path] and not st[STATE_KEY.AUTOMOUNTS][local_path].locked_automount then |
|
st[STATE_KEY.AUTOMOUNTS][local_path].locked_automount = true |
|
local args = ya.quote(ACTION.MOUNT_THEN_JUMP_SUBFOLDER) |
|
.. " " |
|
.. ya.quote(cwd_raw) |
|
.. " " |
|
.. ya.quote(local_path) |
|
.. " " |
|
.. ya.quote(payload.tab) |
|
ya.emit("plugin", { |
|
self._id, |
|
args, |
|
}) |
|
end |
|
end |
|
end) |
|
|
|
-- ps.sub(PUBSUB_KIND.hover, function() |
|
-- local cwd = current_hovered_folder_cwd() |
|
-- if not cwd then |
|
-- return |
|
-- end |
|
-- local cwd_raw = tostring(cwd) |
|
-- if |
|
-- cwd_raw:match( |
|
-- "^" .. is_literal_string(get_state(STATE_KEY.ROOT_MOUNTPOINT) .. "/google-drive:host=gmail.com") |
|
-- ) |
|
-- then |
|
-- enqueue_task(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER, { folder_to_load = cwd_raw, self_load = false }) |
|
-- local args = ya.quote(ACTION.LOAD_GDRIVE_FOLDER) |
|
-- ya.emit("plugin", { |
|
-- self._id, |
|
-- args, |
|
-- }) |
|
-- end |
|
-- end) |
|
ps.sub_remote(PUBSUB_KIND.mounts_changed, function(mounts) |
|
set_state(STATE_KEY.MOUNTS, hex_decode_table(mounts)) |
|
end) |
|
|
|
ps.sub_remote(PUBSUB_KIND.automounts_changed, function(automounts) |
|
set_state(STATE_KEY.AUTOMOUNTS, hex_decode_table(automounts)) |
|
end) |
|
|
|
ps.sub_remote(PUBSUB_KIND.unmounted, function(unmounted_url) |
|
redirect_unmounted_tab_to_home(hex_decode(unmounted_url)) |
|
end) |
|
end |
|
|
|
---@param job {args: unknown[], args: {jump: boolean?, eject: boolean?, force: boolean?, automount: boolean?, disabled: boolean?}} |
|
function M:entry(job) |
|
if not is_cmd_exist("gio") then |
|
error(NOTIFY_MSG.CMD_NOT_FOUND, "gio") |
|
return |
|
end |
|
-- Fallback to pass if in headless session |
|
if not is_in_dbus_session() then |
|
error(NOTIFY_MSG.HEADLESS_DETECTED) |
|
return |
|
end |
|
|
|
if get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.KEYRING then |
|
if not is_cmd_exist(SECRET_TOOL) then |
|
set_state(STATE_KEY.PASSWORD_VAULT, nil) |
|
end |
|
end |
|
if get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.PASS then |
|
if not is_cmd_exist(GPG_TOOL) or not is_cmd_exist(PASS_TOOL) or get_state(STATE_KEY.KEY_GRIP) == nil then |
|
set_state(STATE_KEY.PASSWORD_VAULT, nil) |
|
end |
|
end |
|
local action = job.args[1] |
|
-- Select a device then mount |
|
if action == ACTION.SELECT_THEN_MOUNT then |
|
local jump = job.args.jump or false |
|
mount_action({ jump = jump }) |
|
-- select a device then unmount |
|
elseif action == ACTION.SELECT_THEN_UNMOUNT then |
|
local eject = job.args.eject or false |
|
local force = job.args.force or false |
|
unmount_action(nil, eject, force) |
|
-- remount device within current cwd |
|
elseif action == ACTION.REMOUNT_KEEP_CWD_UNCHANGED then |
|
remount_keep_cwd_unchanged_action(STATE_KEY.CACHED_LOCAL_PATH_DEVICE) |
|
-- select a device then go to its mounted point |
|
elseif action == ACTION.JUMP_TO_DEVICE then |
|
local automount = job.args.automount or false |
|
jump_to_device_mountpoint_action(nil, nil, automount) |
|
elseif action == ACTION.JUMP_BACK_PREV_CWD then |
|
jump_to_prev_cwd_action() |
|
elseif action == ACTION.ADD_MOUNT then |
|
add_or_edit_mount_action() |
|
elseif action == ACTION.EDIT_MOUNT then |
|
add_or_edit_mount_action(true) |
|
elseif action == ACTION.REMOVE_MOUNT then |
|
remove_mount_action() |
|
elseif action == ACTION.AUTOMOUNT_WHEN_CD then |
|
local enabled = not job.args.disabled |
|
toggle_automount_when_cd_action(enabled) |
|
|
|
-- NOTE: Switch to new async when it's merged to yazi nightly |
|
elseif action == ACTION.LOAD_GDRIVE_FOLDER then |
|
load_gdrive_folder_action() |
|
elseif action == ACTION.CACHE_LOCAL_PATH_DEVICE then |
|
local local_path = job.args[2] |
|
if fs.cha(Url(local_path)) then |
|
cache_local_path_device_action(local_path) |
|
end |
|
elseif action == ACTION.MOUNT_THEN_JUMP_SUBFOLDER then |
|
local subfolder_path = job.args[2] |
|
local local_path = job.args[3] |
|
local tab_id = job.args[4] |
|
local local_path_cha, _ = fs.cha(Url(local_path)) |
|
local cached_device = get_state(STATE_KEY.AUTOMOUNTS)[local_path] |
|
if local_path_cha and local_path_cha.is_dir then |
|
-- NOTE: Skip automount |
|
cached_device.locked_automount = false |
|
set_state_table(STATE_KEY.AUTOMOUNTS, local_path, cached_device) |
|
return |
|
end |
|
if cached_device then |
|
-- Update cached device with new data |
|
local new_cached_device = |
|
remount_keep_cwd_unchanged_action(STATE_KEY.AUTOMOUNTS, subfolder_path, tab_id, true) |
|
if new_cached_device then |
|
cached_device = new_cached_device |
|
end |
|
end |
|
cached_device.locked_automount = false |
|
set_state_table(STATE_KEY.AUTOMOUNTS, local_path, cached_device) |
|
end |
|
-- TODO: remove this after next yazi released |
|
(ui.render or ya.render)() |
|
end |
|
|
|
return M
|
|
|