Фоновое копирование с помощью FastCopy
Posted: Sat 16 Nov, 2019 19:23
by DV
Поскольку плагин Background Copy не развивается, а необходимость копирования в фоне есть, было принято волевое решение реализовать копирование в фоне с помощью макроса.
Особенности данного макроса:
Он же:
Особенности данного макроса:
- Код данного макроса базируется на макросах Plugin_FastCopy & Plugin_TeraCopy (C) SimSU
- Почему именно FastCopy? Потому что это "рабочая лошадка", невзирая на его невзрачный внешний вид. TeraCopy выглядит гораздо приятнее, но при копировании файлов порой возникают проблемы. Это неприемлемо.
- Особенности реализации:
- Для копирования/перемещения/удаления в фоне используются сочетания Ctrl+Shift+F5, Ctrl+Shift+F6 и Ctrl+Shift+F8. Первоначально использовались комбинации с Alt вместо Ctrl, поскольку зажатая клавиша Ctrl (которую при вызове макроса не всегда успеваешь быстро отпустить) воспринимается FastCopy как необходимость подтверждения операции над файлами. Однако, макрос с версии 1.2 стал показывать диалоги подтверждения операций (в окне FAR Manager), что позволило абстрагировать клавиатурные сочетания для вызова макроса от запуска FastCopy.
- Данный макрос умеет копировать с временной панели в качестве источника.
- В диалоге подтверждения кнопка [FastCopy...] показывает окно FastCopy, не начиная выполнять операции над файлами. Это позволяет изменить настройки FastCopy, отредактировать список переданных файлов, выполнить операции над файлами нажатием кнопки Execute или же закрыть окно FastCopy, отменив операцию.
- Поскольку FastCopy позволяет указать список файлов (для копирования/перемещения/удаления) прямо в командной строке, то при суммарной длине командной строки менее 321024 символов список файлов передаётся через неё. В противном случае список файлов передаётся внутри временного файла.
- Переменная MaxCmdLine (внутри макроса или файла настроек) задаёт максимальную длину командной строки, при которой временный файл не используется. Мои эксперименты показали, что в Windows 7, 8.1 и 10 длина в 321024 символа обрабатывается успешно, тогда как в Windows XP приходится ограничиваться длиной в 2048 символа.
- Переменная FastCopyPath (внутри макроса или файла настроек) указывает путь к FastCopy.exe
- Начиная с версии 1.3.2, если макрос не находит FastCopy.exe, то по нажатию кнопки [Edit...] он автоматически создаёт файл настроек Plugin.FastCopy.cfg рядом с Plugin.FastCopy.lua, и при дальнейших вызовах читает настройки именно из этого файла.
- Особенности настройки FastCopy:
- Поскольку меня интересовала возможность очереди фоновых операций (когда две разные операции копирования не происходят параллельно), значение настройки Option -> Main Settings -> Parallel running Settings -> Max number of running FastCopy procs я выставил в "1". При этом только один экземпляр FastCopy выполняет операцию копирования - а запущенные за ним экземпляры ждут своей очереди.
- Поскольку при настройках FastCopy по умолчанию копирование большого файла с флешки на компьютер практически блокирует FAR Manager (я так понимаю, FastCopy отъедает все ресурсы, связанные с файловой системой), рекомендую изменить следующие значения в настройках:
- Option -> Main Settings -> Defaults -> Speed Control выставить в "Auto Slow" вместо "Full Speed"
- Option -> Main Settings -> I/O Settings -> OverLap I/O Num выставить в "1" вместо "20"
- Option -> Main Settings -> I/O Settings -> Buffer Size(MB) выставить в "64" вместо "128"
- При попытке копирования папки саму в себя (когда пути в левой и правой панели одинаковые) FastCopy предлагает автоматически переименовать результирующую папку - таким образом, создавая копию существующей папки. Чтобы запретить копирование папки саму в себя, убираем галочку с Option -> Main Settings -> Copy/Move Options -> Use a modified name, if destdir same as src`s parent.
- Настройка I/O Settings -> OverLap I/O Num = 1 исправляет ошибку записи/верификации на некоторые SD/MicroSD карты памяти. (Согласно разработчику, "SD card may be causing problems with asynchronous/overlapped I/O. Please set "OverLap I/O num" to 1 and try again").
- -- Copy/Move/Delete via FastCopy
- -- version 1.4.0
- -- Based on Plugin_FastCopy & Plugin_TeraCopy by SimSU
- -- See https://forum.farmanager.com/viewtopic.php?f=15&t=11880
- local ffi = require("ffi")
- local C = ffi.C
- ffi.cdef[[
- BOOL SetForegroundWindow(HWND hWnd);
- ]]
- local Cfg = {
- -- Note: if Ctrl is pressed while FastCopy is starting, FastCopy displays the confirmation dialog!
- KeyCopy = 'CtrlShiftF5',
- KeyMove = 'CtrlShiftF6',
- KeyDelete = 'CtrlShiftF8',
- FastCopyPath = '%FARHOME%\\..\\Progs\\FastCopy\\FastCopy.exe',
- CopyCmd = '/cmd=diff /estimate /verify',
- MoveCmd = '/cmd=move /estimate /verify',
- DeleteCmd = '/cmd=delete /no_confirm_del',
- MaxCmdLine = 32*1024, -- otherwise a temporary file is used to pass the filenames
- -- (!) looks like in Windows XP it is limited to 2048
- TmpFileTimeout = 3000, -- number of milliseconds before the temporary file is deleted
- SetFocusTimeout = 400, -- number of milliseconds before returning the focus to FAR
- -- (this is a rough time for a "cold start" of FastCopy)
- ShowConfirmationDialogs = true, -- in FAR Manager
- FastCopyInBackground = true, -- FastCopy's window is minimized
- CfgVersion = '1.1'
- };
- local Msg = {
- -- Macro Descriptions
- MacroDescr = {
- Copy = 'Background Copy via FastCopy',
- Move = 'Background Move via FastCopy',
- Delete = 'Background Delete via FastCopy'
- },
- -- Error Messages
- Error = {
- Title = 'Plugin.FastCopy - Error',
- TitleCfg = 'Plugin.FastCopy.cfg - Error',
- PathToFastCopy = 'Path to FastCopy does not exist!\n\1\n"%s"',
- TmpFileList = 'Failed to create a temporary file',
- ConfigFile = 'Syntax error in "%s":\n',
- ButtonsOkEdit = 'OK;Edit…'
- },
- -- Copy Dialog
- CopyDlg = {
- Title = 'Copy via FastCopy',
- Msg1 = 'Copy "%s" to\n%s',
- MsgN = 'Copy %d items to\n%s\n\1',
- Buttons = '&Copy;Ca&ncel;&FastCopy…;Config…'
- },
- -- Move Dialog
- MoveDlg = {
- Title = 'Move via FastCopy',
- Msg1 = 'Move "%s" to\n%s',
- MsgN = 'Move %d items to\n%s\n\1',
- Buttons = '&Move;Ca&ncel;&FastCopy…;Config…'
- },
- -- Delete Dialog
- DeleteDlg = {
- Title = 'Delete via FastCopy',
- Msg1File = 'Do you wish to delete the file\n"%s"',
- Msg1Dir = 'Do you wish to delete the folder\n"%s"',
- MsgN = 'Do you wish to delete %d items\n\1',
- Buttons = '&Delete;Ca&ncel;&FastCopy…;Config…'
- }
- };
- local Runtime = {}
- Runtime.PathToThisLuaScript, Runtime.ExeCounter = ...
- local function normpath(p)
- return far.ConvertPath(p, 'CPM_FULL')
- end
- local function get_fdir(fpath)
- return mf.fsplit(fpath, 0x03)
- end
- local function get_fname(fpath)
- return mf.fsplit(fpath, 0x0C)
- end
- local function full_path(dir, filename)
- if filename:len() >= 2 then
- local c = filename:sub(2, 2)
- if c == ':' or ((c == '\\' or c == '/') and c == filename:sub(1, 1)) then
- -- filename is either "X:..." or "\\..." or "//..."
- return filename
- end
- end
- if dir == nil or dir == '' then
- return filename
- end
- if dir:sub(-1, -1) == '\\' then
- return dir .. filename
- end
- return dir .. '\\' .. filename
- end
- local function is_dir(fpath)
- local attr = win.GetFileAttr(fpath)
- return attr ~= nil and attr:find('d') ~= nil
- end
- local function ShowError(msg, --[[optional]] buttons)
- return far.Message(msg, Msg.Error.Title, buttons or ';Ok', 'w')
- end
- local function ShowCfgError(msg, --[[optional]] buttons)
- return far.Message(msg, Msg.Error.TitleCfg, buttons or ';Ok', 'w')
- end
- local function toRawLuaString(s)
- return s:gsub('\\', '\\\\')
- end
- local function SaveConfig(pathToCfgFile) -- saves the Cfg values
- local isSaved = false
- local contentFmt = [[local Cfg = {
- KeyCopy = '%s',
- KeyMove = '%s',
- KeyDelete = '%s',
- FastCopyPath = '%s',
- CopyCmd = '%s',
- MoveCmd = '%s',
- DeleteCmd = '%s',
- MaxCmdLine = %s,
- TmpFileTimeout = %s,
- SetFocusTimeout = %s,
- ShowConfirmationDialogs = %s,
- FastCopyInBackground = %s,
- CfgVersion = '%s'
- };
- ]];
- local content = contentFmt:format(
- toRawLuaString(Cfg.KeyCopy),
- toRawLuaString(Cfg.KeyMove),
- toRawLuaString(Cfg.KeyDelete),
- toRawLuaString(Cfg.FastCopyPath),
- toRawLuaString(Cfg.CopyCmd),
- toRawLuaString(Cfg.MoveCmd),
- toRawLuaString(Cfg.DeleteCmd),
- tostring(Cfg.MaxCmdLine),
- tostring(Cfg.TmpFileTimeout),
- tostring(Cfg.SetFocusTimeout),
- tostring(Cfg.ShowConfirmationDialogs),
- tostring(Cfg.FastCopyInBackground),
- tostring(Cfg.CfgVersion)
- );
- local cfgFile = io.open(pathToCfgFile, 'w')
- if cfgFile then
- cfgFile:write(content)
- cfgFile:close()
- isSaved = true
- end
- return isSaved
- end
- local function mergeConfig(fullConfig, partialConfig)
- if partialConfig ~= nil then
- for i, v in pairs(fullConfig) do
- if type(v) == 'table' then
- mergeSettings(v, partialConfig[i])
- elseif partialConfig[i] ~= nil then
- fullConfig[i] = partialConfig[i]
- end
- end
- end
- end
- local function LoadConfig(pathToCfgFile) -- updates the Cfg values
- local isLoaded = false
- local hasError = false
- local cfgFile = io.open(pathToCfgFile, 'r')
- if cfgFile then
- local content = cfgFile:read('*a')
- cfgFile:close()
- local code, err = loadstring(content .. 'return Cfg;')
- if not err then
- local status, result = pcall(code)
- if status then
- local ownCfgVersion = Cfg.CfgVersion
- local loadedCfgVersion = result.CfgVersion
- mergeConfig(Cfg, result)
- if ownCfgVersion ~= loadedCfgVersion then
- Cfg.CfgVersion = ownCfgVersion
- SaveConfig(pathToCfgFile) -- updating the config file
- end
- isLoaded = true
- else
- err = result
- end
- end
- if err then
- hasError = true
- if ShowCfgError(Msg.Error.ConfigFile:format(pathToCfgFile) .. err, Msg.Error.ButtonsOkEdit) == 2 then
- if editor.Editor(pathToCfgFile, nil, nil, nil, nil, nil, nil) == far.Flags.EEC_MODIFIED then
- isLoaded, hasError = LoadConfig(pathToCfgFile)
- end
- end
- end
- end
- return isLoaded, hasError
- end
- local function getPathToCfgFile()
- local pathToCfgFile, luaExt = Runtime.PathToThisLuaScript:match('(.*)(%..*)')
- return pathToCfgFile .. '.cfg'
- end
- Runtime.PathToCfgFile = getPathToCfgFile()
- Runtime.IsConfigLoaded, Runtime.HasConfigError = LoadConfig(Runtime.PathToCfgFile)
- local function GetPanelItems()
- local files = {}
- local idx = nil
- local dir = panel.GetPanelDirectory(nil, 1).Name
- local cnt = APanel.SelCount
- if cnt == 0 then -- no selected items
- cnt = 1
- files[1] = panel.GetCurrentPanelItem(nil, 1).FileName
- else
- idx = {}
- for i = 1, cnt do
- files[i] = panel.GetSelectedPanelItem(nil, 1, i).FileName
- idx[i] = i
- end
- end
- return {
- Dir = dir, -- panel's directory
- Count = cnt, -- number of items (current or selected)
- Files = files, -- current or selected item(s)
- Idx = idx -- selected indexes or nil
- }
- end
- local function ClearPanelSelection(idx)
- if idx ~= nil then
- panel.ClearSelection(nil, 1, idx)
- panel.RedrawPanel(nil, 1)
- end
- end
- local function SaveFilesListToTmpFile(filesList)
- local content = table.concat(filesList, '\r\n')
- content = win.Utf8ToUtf16(content)
- local tmpFileName = far.MkTemp()
- local tmpFile = io.open(tmpFileName, 'w+b')
- if tmpFile then
- local bom = string.char(0xFF,0xFE)
- tmpFile:write(bom, content)
- tmpFile:close()
- return tmpFileName
- end
- return ''
- end
- local function DeleteTmpFileDelayed(fileName, delay_ms)
- far.Timer( delay_ms, function(timer) timer:Close(); win.DeleteFile(fileName) end )
- end
- local function SetFocusDelayed(delay_ms)
- far.Timer( delay_ms, function(timer) timer:Close(); local farWnd = far.AdvControl(far.Flags.ACTL_GETFARHWND); C.SetForegroundWindow(farWnd) end )
- end
- local function Configure()
- if not mf.fexist(Runtime.PathToCfgFile) then
- SaveConfig(Runtime.PathToCfgFile)
- end
- if editor.Editor(Runtime.PathToCfgFile, nil, nil, nil, nil, nil, nil) == far.Flags.EEC_MODIFIED then
- Runtime.IsConfigLoaded, Runtime.HasConfigError = LoadConfig(Runtime.PathToCfgFile)
- end
- end
- local function ConfirmOperation(cmd, items, dstDir)
- local dlgmsg = nil
- local msg = ''
- local flags = 'l' -- 'l' = left align
- local appendFilesList = function(msg, items)
- local n = 10
- if items.Count < n then
- n = items.Count
- end
- for i = 1, n do
- msg = msg .. '\n ' .. items.Files[i]
- end
- if n < items.Count then
- msg = msg .. '\n...'
- end
- return msg
- end
- if cmd == Cfg.DeleteCmd then
- dlgmsg = Msg.DeleteDlg
- if items.Count == 1 then
- local f = full_path(items.Dir, items.Files[1])
- if is_dir(f) then
- msg = dlgmsg.Msg1Dir:format(items.Files[1])
- else
- msg = dlgmsg.Msg1File:format(items.Files[1])
- end
- flags = ''
- else
- msg = dlgmsg.MsgN:format(items.Count)
- msg = appendFilesList(msg, items)
- end
- flags = flags .. 'w' -- 'w' = warning
- else
- if cmd == Cfg.CopyCmd then
- dlgmsg = Msg.CopyDlg
- else -- cmd == Cfg.MoveCmd
- dlgmsg = Msg.MoveDlg
- end
- if items.Count == 1 then
- msg = dlgmsg.Msg1:format(items.Files[1], dstDir)
- else
- msg = dlgmsg.MsgN:format(items.Count, dstDir)
- msg = appendFilesList(msg, items)
- end
- end
- return far.Message(msg, dlgmsg.Title, dlgmsg.Buttons, flags)
- end
- local function InvokeFastCopy(cmd)
- if Runtime.HasConfigError then
- Runtime.IsConfigLoaded, Runtime.HasConfigError = LoadConfig(Runtime.PathToCfgFile)
- return -1
- end
- local pathToFastCopy = normpath(Cfg.FastCopyPath:gsub('%%(%w+)%%', win.GetEnv))
- if not mf.fexist(pathToFastCopy) then
- pathToFastCopy = win.SearchPath(nil, get_fname(Cfg.FastCopyPath), nil) or pathToFastCopy
- if not mf.fexist(pathToFastCopy) then
- if ShowError(Msg.Error.PathToFastCopy:format(pathToFastCopy), Msg.Error.ButtonsOkEdit) == 2 then
- if not mf.fexist(Runtime.PathToCfgFile) then
- SaveConfig(Runtime.PathToCfgFile)
- end
- if editor.Editor(Runtime.PathToCfgFile, nil, nil, nil, nil, nil, nil, 6, 21) == far.Flags.EEC_MODIFIED then
- Runtime.IsConfigLoaded, Runtime.HasConfigError = LoadConfig(Runtime.PathToCfgFile)
- end
- end
- return -1
- end
- end
- local items = GetPanelItems() -- source files
- if items.Count == 1 then
- if items.Files[1] == '..' or items.Files[1] == '.' then
- return -1
- end
- end
- local inputFiles = ''
- local tmpFileName = ''
- local dstDir = panel.GetPanelDirectory(nil,0).Name -- destination
- if Cfg.ShowConfirmationDialogs then
- local nConfirm = ConfirmOperation(cmd, items, dstDir)
- if nConfirm == 1 then -- [Copy]/[Move]/[Delete] pressed
- cmd = cmd .. ' /auto_close'
- if not Cfg.FastCopyInBackground then
- cmd = cmd .. ' /open_window'
- end
- elseif nConfirm == 3 then -- [FastCopy…] pressed
- cmd = cmd .. ' /no_exec /open_window'
- elseif nConfirm == 4 then -- [Config…] pressed
- Configure()
- return -1
- else -- [Cancel] or Esc pressed
- return -1
- end
- else -- no FAR's confirmation dialog
- cmd = cmd .. ' /auto_close'
- if not Cfg.FastCopyInBackground then
- cmd = cmd .. ' /open_window'
- end
- end
- local isFastCopyMinimized = true
- if cmd:find(' /open_window') ~= nil then
- isFastCopyMinimized = false
- end
- if items.Count == 1 then
- inputFiles = '"' .. full_path(items.Dir, items.Files[1]) .. '"'
- else
- local filesList = {}
- for idx, val in ipairs(items.Files) do
- filesList[idx] = '"' .. full_path(items.Dir, val) .. '"'
- end
- inputFiles = table.concat(filesList, ' ')
- if inputFiles:len() > Cfg.MaxCmdLine - 512 then
- tmpFileName = SaveFilesListToTmpFile(filesList)
- if tmpFileName == '' then
- ShowError(Msg.Error.TmpFileList)
- return -1
- end
- inputFiles = '/srcfile_w="' .. tmpFileName .. '"'
- end
- end
- ClearPanelSelection(items.Idx)
- if dstDir:sub(-1, -1) ~= '\\' then
- dstDir = dstDir .. '\\'
- end
- local fastcopyParams = cmd .. ' ' .. inputFiles .. ' /to="' .. dstDir .. '"'
- local n = win.ShellExecute(nil, nil, pathToFastCopy, fastcopyParams, nil, nil)
- if tmpFileName ~= '' then
- DeleteTmpFileDelayed(tmpFileName, Cfg.TmpFileTimeout)
- end
- if isFastCopyMinimized then
- SetFocusDelayed(Cfg.SetFocusTimeout)
- end
- return n
- end
- local function IsActivePanelOK()
- return APanel.Visible and (not APanel.Plugin or (band(APanel.OPIFlags, far.Flags.OPIF_REALNAMES) ~= 0))
- end
- local function IsPassivePanelOK()
- return PPanel.Visible and not PPanel.Plugin
- end
- local function CheckCond(checkPassivePanel)
- return IsActivePanelOK() and (not checkPassivePanel or IsPassivePanelOK())
- end
- Macro {
- area = "Shell";
- key = Cfg.KeyCopy;
- description = Msg.MacroDescr.Copy;
- condition = function() return CheckCond(true) end;
- action = function() InvokeFastCopy(Cfg.CopyCmd) end;
- }
- Macro {
- area = "Shell";
- key = Cfg.KeyMove;
- description = Msg.MacroDescr.Move;
- condition = function() return CheckCond(true) end;
- action = function() InvokeFastCopy(Cfg.MoveCmd) end;
- }
- Macro {
- area = "Shell";
- key = Cfg.KeyDelete;
- description = Msg.MacroDescr.Delete;
- condition = function() return CheckCond(false) end;
- action = function() InvokeFastCopy(Cfg.DeleteCmd) end;
- }
GeSHi © Codebox Plus Extension
Он же:
- Plugin.FastCopy.1.4.0.7z
- (3.83 KiB) Downloaded 163 times