diff options
Diffstat (limited to 'yazi/plugins/compress.yazi/main.lua')
| -rw-r--r-- | yazi/plugins/compress.yazi/main.lua | 577 |
1 files changed, 577 insertions, 0 deletions
diff --git a/yazi/plugins/compress.yazi/main.lua b/yazi/plugins/compress.yazi/main.lua new file mode 100644 index 0000000..440f6e2 --- /dev/null +++ b/yazi/plugins/compress.yazi/main.lua @@ -0,0 +1,577 @@ +--- @since 25.12.29 + +-- Check for windows +local is_windows = ya.target_family() == "windows" + +-- Define default flags and strings +local is_password, is_encrypted, is_level, is_silent_success = false, false, false, false +local default_extension = "zip" + +-- Allow dots when matching file extension arguments +local function extension_pattern(ext) + return "%." .. ext:gsub("%.", "%%.") .. "$" +end + +-- Function to check valid filename +local function is_valid_filename(name) + -- Trim whitespace from both ends + name = name:match("^%s*(.-)%s*$") + if name == "" then + return false + end + if is_windows then + -- Windows forbidden chars and reserved names + if name:find('[<>:"/\\|%?%*]') then + return false + end + else + -- Unix forbidden chars + if name:find("/") or name:find("%z") then + return false + end + end + return true +end + +-- Function to send notifications +local function notify(message, level) + ya.notify({ + title = "Archive", + content = message, + level = level, + timeout = 5, + }) +end + +-- Function to check if command is available +local function is_command_available(cmd) + local stat_cmd + if is_windows then + stat_cmd = string.format("where %s > nul 2>&1", cmd) + else + stat_cmd = string.format("command -v %s >/dev/null 2>&1", cmd) + end + return os.execute(stat_cmd) +end + +-- Function to find first available command from list +local function find_command_name(cmd_list) + for _, cmd in ipairs(cmd_list) do + if is_command_available(cmd) then + return cmd + end + end + return cmd_list[1] -- Return first command as fallback +end + +-- Function to append filename to its parent directory url +local function combine_url(path, file) + local path_url = Url(path) + local file_url = Url(file) + return tostring(path_url:join(file_url)) +end + +-- Function to make a table of selected or hovered files: path = filenames +local selected_or_hovered = ya.sync(function() + local tab = cx.active + local paths = {} + local names = {} + local path_fnames = {} + + for _, u in pairs(tab.selected) do + paths[#paths + 1] = tostring(u.parent) + names[#names + 1] = tostring(u.name) + end + + if #paths == 0 and tab.current.hovered then + paths[1] = tostring(tab.current.hovered.url.parent) + names[1] = tostring(tab.current.hovered.name) + end + + for idx, name in ipairs(names) do + local path = paths[idx] + if not path_fnames[path] then + path_fnames[path] = {} + end + table.insert(path_fnames[path], name) + end + + return path_fnames, names, tostring(tab.current.cwd) +end) + +-- Function to cleanup temporary directory +local function cleanup_temp_dir(temp_dir) + local status, err = fs.remove("dir_all", Url(temp_dir)) + if not status then + notify( + string.format("Failed to clean up temporary directory %s, error: %s", ya.quote(temp_dir), tostring(err)), + "error" + ) + return false + end + return true +end + +-- Function for compression level +local function add_compression_level(target_args, level_arg, level_value) + if type(level_arg) == "table" then + -- Insert each element except last + for i = 1, #level_arg - 1 do + table.insert(target_args, i, level_arg[i]) + end + -- Add the level value with the last element + table.insert(target_args, #level_arg, level_arg[#level_arg] .. level_value) + else + -- Single string argument + table.insert(target_args, 1, level_arg .. level_value) + end +end + +-- Function for password handling +local function get_password_args(archive_cmd, encrypted, header_arg) + local output_password, event = ya.input({ + title = "Enter password:", + obscure = true, + pos = { "top-center", y = 3, w = 40 }, + }) + if event ~= 1 or output_password == "" then + return nil + end + -- Handling for RAR with encryption + if archive_cmd == "rar" and encrypted then + return { header_arg .. output_password } + end + return { "-P" .. output_password } +end + +-- Table of archive commands +local archive_commands = { + ["%.zip$"] = { + { command = "zip", args = { "-r" }, level_arg = "-", level_min = 0, level_max = 9, passwordable = true }, + { + command = { "7z", "7zz", "7za" }, + args = { "a", "-tzip" }, + level_arg = "-mx=", + level_min = 0, + level_max = 9, + passwordable = true, + }, + { + command = { "tar", "bsdtar" }, + args = { "-caf" }, + level_arg = { "--option", "compression-level=" }, + level_min = 1, + level_max = 9, + }, + }, + ["%.7z$"] = { + { + command = { "7z", "7zz", "7za" }, + args = { "a" }, + level_arg = "-mx=", + level_min = 0, + level_max = 9, + header_arg = "-mhe=on", + passwordable = true, + }, + }, + ["%.rar$"] = { + { + command = "rar", + args = { "a" }, + level_arg = "-m", + level_min = 0, + level_max = 5, + header_arg = "-hp", + passwordable = true, + }, + }, + ["%.tar%.gz$"] = { + { + command = { "tar", "bsdtar" }, + args = { "rpf" }, + level_arg = "-", + level_min = 1, + level_max = 9, + compress = "gzip", + }, + { + command = { "tar", "bsdtar" }, + args = { "rpf" }, + level_arg = "-mx=", + level_min = 1, + level_max = 9, + compress = "7z", + compress_args = { "a", "-tgzip" }, + }, + { + command = { "tar", "bsdtar" }, + args = { "-czf" }, + level_arg = { "--option", "gzip:compression-level=" }, + level_min = 1, + level_max = 9, + }, + }, + ["%.tar%.xz$"] = { + { + command = { "tar", "bsdtar" }, + args = { "rpf" }, + level_arg = "-", + level_min = 1, + level_max = 9, + compress = "xz", + }, + { + command = { "tar", "bsdtar" }, + args = { "rpf" }, + level_arg = "-mx=", + level_min = 1, + level_max = 9, + compress = "7z", + compress_args = { "a", "-txz" }, + }, + { + command = { "tar", "bsdtar" }, + args = { "-cJf" }, + level_arg = { "--option", "xz:compression-level=" }, + level_min = 1, + level_max = 9, + }, + }, + ["%.tar%.bz2$"] = { + { + command = { "tar", "bsdtar" }, + args = { "rpf" }, + level_arg = "-", + level_min = 1, + level_max = 9, + compress = "bzip2", + }, + { + command = { "tar", "bsdtar" }, + args = { "rpf" }, + level_arg = "-mx=", + level_min = 1, + level_max = 9, + compress = "7z", + compress_args = { "a", "-tbzip2" }, + }, + { + command = { "tar", "bsdtar" }, + args = { "-cjf" }, + level_arg = { "--option", "bzip2:compression-level=" }, + level_min = 1, + level_max = 9, + }, + }, + ["%.tar%.zst$"] = { + { + command = { "tar", "bsdtar" }, + args = { "rpf" }, + level_arg = "-", + level_min = 1, + level_max = 22, + compress = "zstd", + compress_args = { "--ultra" }, + }, + }, + ["%.tar%.lz4$"] = { + { + command = { "tar", "bsdtar" }, + args = { "rpf" }, + level_arg = "-", + level_min = 1, + level_max = 12, + compress = "lz4", + }, + }, + ["%.tar%.lha$"] = { + { + command = { "tar", "bsdtar" }, + args = { "rpf" }, + level_arg = "-o", + level_min = 5, + level_max = 7, + compress = "lha", + compress_args = { "-a" }, + }, + }, + ["%.tar$"] = { + { command = { "tar", "bsdtar" }, args = { "rpf" } }, + }, +} + +-- Function for command matching +local function find_archive_command(output_name) + for pattern, cmd_list in pairs(archive_commands) do + if output_name:match(pattern) then + for _, cmd in ipairs(cmd_list) do + -- Check if archive command is available + local cmd_name = type(cmd.command) == "table" and find_command_name(cmd.command) or cmd.command + if is_command_available(cmd_name) then + -- Check if compress command (if listed) is available + if not cmd.compress or is_command_available(cmd.compress) then + return { + cmd = cmd_name, + args = cmd.args, + compress = cmd.compress or "", + level_arg = cmd.level_arg or "", + level_min = cmd.level_min, + level_max = cmd.level_max, + header_arg = cmd.header_arg or "", + passwordable = cmd.passwordable or false, + compress_args = cmd.compress_args or {}, + } + end + end + end + -- Pattern matched but no suitable command found + return nil + end + end + -- No pattern matched - unsupported extension + return false +end + +return { + entry = function(_, job) + -- Parse flags and default extension + if job.args then + for _, arg in ipairs(job.args) do + if arg:match("^%-(%w+)$") then + -- Handle combined flags (e.g., -phl) + for flag in arg:sub(2):gmatch(".") do + if flag == "p" then + is_password = true + elseif flag == "h" then + is_encrypted = true + elseif flag == "l" then + is_level = true + elseif flag == "s" then + is_silent_success = true + end + end + elseif arg:match("^[%w%.]+$") then + -- Handle default extension (e.g., 7z, zip) + if archive_commands[extension_pattern(arg)] then + default_extension = arg + else + notify(string.format("Unsupported extension: %s", arg), "warn") + end + else + notify(string.format("Unknown argument: %s", arg), "warn") + end + end + end + + -- Exit visual mode + ya.emit("escape", { visual = true }) + + -- Define file table and output_dir (pwd) + local path_fnames, fnames, output_dir = selected_or_hovered() + + -- Get archive filename + local output_name, event = ya.input({ + title = "Create archive:", + pos = { "top-center", y = 3, w = 40 }, + }) + if event ~= 1 then + return + end + + -- Determine the default name for the archive + local default_name = #fnames == 1 and fnames[1] or Url(output_dir).name + output_name = output_name == "" and string.format("%s.%s", default_name, default_extension) or output_name + + -- Add default extension if none is specified + if not output_name:match("%.%w+$") then + output_name = string.format("%s.%s", output_name, default_extension) + end + + -- Validate the final archive filename + if not is_valid_filename(output_name) then + notify("Invalid archive filename", "error") + return + end + + -- Command matching + local archive_config = find_archive_command(output_name) + if archive_config == false then + notify("Unsupported file extension", "error") + return + elseif not archive_config then + notify("Could not find a suitable archive program for the selected file extension", "error") + return + end + + -- Extract configuration + local archive_cmd = archive_config.cmd + local archive_args = archive_config.args + local archive_compress = archive_config.compress + local archive_level_arg = is_level and archive_config.level_arg or "" + local archive_level_min = archive_config.level_min + local archive_level_max = archive_config.level_max + local archive_header_arg = is_encrypted and archive_config.header_arg or "" + local archive_passwordable = archive_config.passwordable + local archive_compress_args = archive_config.compress_args + + -- Password handling + if archive_passwordable and is_password then + local password_args = get_password_args(archive_cmd, is_encrypted, archive_header_arg) + if password_args then + for _, arg in ipairs(password_args) do + table.insert(archive_args, arg) + end + end + end + + -- Add header arg if selected for 7z + if is_encrypted and archive_header_arg ~= "" and archive_cmd ~= "rar" then + table.insert(archive_args, archive_header_arg) + end + + -- Use extracted compression level + if archive_level_arg ~= "" and is_level then + local output_level, level_event = ya.input({ + title = string.format("Enter compression level (%s - %s)", archive_level_min, archive_level_max), + pos = { "top-center", y = 3, w = 40 }, + }) + if level_event ~= 1 then + return + end + -- Validate user input for compression level + local level_num = tonumber(output_level) + if level_num and level_num >= archive_level_min and level_num <= archive_level_max then + local target_args = archive_compress == "" and archive_args or archive_compress_args + add_compression_level(target_args, archive_level_arg, output_level) + else + notify("Invalid level specified. Using defaults.", "warn") + end + end + + -- Store the original output name for later use + local original_name = output_name + + -- If compression is needed, adjust the output name to exclude extensions like ".tar" + if archive_compress ~= "" then + output_name = output_name:match("(.*%.tar)") or output_name + end + + -- Create a temporary directory for intermediate files + local temp_dir_name = ".tmp_compress" + local temp_dir = combine_url(output_dir, temp_dir_name) + temp_dir = tostring(fs.unique_name(Url(temp_dir))) + + -- Attempt to create the temporary directory + local temp_dir_status, temp_dir_err = fs.create("dir_all", Url(temp_dir)) + if not temp_dir_status then + -- Notify the user if the temporary directory creation fails + notify(string.format("Failed to create temp directory, error code: %s", tostring(temp_dir_err)), "error") + return + end + + -- Define the temporary output file path within the temporary directory + local temp_output_url = combine_url(temp_dir, output_name) + + -- Add files to the output archive + for filepath, filenames in pairs(path_fnames) do + -- Execute the archive command for each path and its respective files + local archive_status, archive_err = + Command(archive_cmd):arg(archive_args):arg(temp_output_url):arg(filenames):cwd(filepath):spawn():wait() + if not archive_status or not archive_status.success then + -- Notify the user if the archiving process fails and clean up the temporary directory + notify( + string.format( + "Failed to create archive %s with '%s', error: %s", + ya.quote(output_name), + archive_cmd, + tostring(archive_err) + ), + "error" + ) + cleanup_temp_dir(temp_dir) + return + end + end + + -- If compression is required, execute the compression command + if archive_compress ~= "" then + local compress_status, compress_err + + -- Check if using 7z for compression (requires output file argument) + if archive_compress:match("^7z") then + local compressed_output = combine_url(temp_dir, original_name) + compress_status, compress_err = Command(archive_compress) + :arg(archive_compress_args) + :arg(compressed_output) + :arg(temp_output_url) + :spawn() + :wait() + else + -- Native compression tools (gzip, xz, bzip2, etc.) compress in-place + compress_status, compress_err = + Command(archive_compress):arg(archive_compress_args):arg(temp_output_url):spawn():wait() + end + + if not compress_status or not compress_status.success then + -- Notify the user if the compression process fails and clean up the temporary directory + notify( + string.format( + "Failed to compress archive %s with '%s', error: %s", + ya.quote(output_name), + archive_compress, + tostring(compress_err) + ), + "error" + ) + cleanup_temp_dir(temp_dir) + return + end + end + + -- Move the final file from the temporary directory to the output directory + local final_output_url = combine_url(output_dir, original_name) + local temp_url_processed = combine_url(temp_dir, original_name) + final_output_url = tostring(fs.unique_name(Url(final_output_url))) + local from, to = Url(temp_url_processed), Url(final_output_url) + local move_status, move_err = fs.rename(from, to) + if not move_status then + if move_err and move_err.kind == "CrossesDevices" then + local copy_status, copy_err = fs.copy(from, to) + if not copy_status then + notify( + string.format( + "Failed to copy across devices %s to %s, error: %s", + ya.quote(from.name), + ya.quote(to.name), + copy_err and tostring(copy_err.kind) or "unknown" + ), + "error" + ) + cleanup_temp_dir(temp_dir) + return + end + else + notify( + string.format( + "Failed to move %s to %s, error: %s", + ya.quote(from.name), + ya.quote(to.name), + move_err and tostring(move_err.kind) or "unknown" + ), + "error" + ) + cleanup_temp_dir(temp_dir) + return + end + end + + -- Cleanup the temporary directory after successful operation + cleanup_temp_dir(temp_dir) + + -- Notify user of success + if not is_silent_success then + notify(string.format("Successfully created archive: %s", ya.quote(to.name)), "info") + end + end, +} |
