Merge pull request #234 from schwerdf/master

New script: 'transfer_hierarchy'.
This commit is contained in:
wpferguson
2020-03-16 11:10:06 -04:00
committed by GitHub
2 changed files with 359 additions and 0 deletions

View File

@@ -59,6 +59,7 @@ rate_group|Yes|LMW|Apply or remove a star rating from grouped images
rename-tags|Yes|LMW|Change a tag name
select_untagged|Yes|LMW|Enable selection of untagged images
slideshowMusic|No|L|Play music during a slideshow
transfer_hierarchy|Yes|LMW|Image move/copy preserving directory hierarchy
video_ffmpeg|No|LMW|Export video from darktable
### Example Scripts

358
contrib/transfer_hierarchy.lua Executable file
View File

@@ -0,0 +1,358 @@
--[[
TRANSFER HIERARCHY
Allows the moving or copying of images from one directory
tree to another, while preserving the existing hierarchy.
AUTHOR
August Schwerdfeger (august@schwerdfeger.name)
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
None.
USAGE
darktable's native operations for moving and copying images in
batches allow only one directory to be specified as the destination
for each batch. Those wanting to move or copy images from a _hierarchy_
of directories within darktable while preserving the directory structure,
must take the laborious step of performing the operation one individual
directory at a time.
This module allows the intact moving and copying of whole directory trees.
It was designed for the specific use case of rapidly transferring images
from a customary source (e.g., a staging directory on the local disk)
to a customary destination (e.g., a directory on a NAS device).
Instructions for operation:
1. Select the set of images you want to copy.
2. Click the "calculate" button. This will calculate the
lowest directory in the hierarchy that contains every selected
file (i.e., the common prefix of all the images' pathnames), and
write its path into the "existing root" text box.
3. If (a) you have specified the "customary source root" and "customary
destination root" preferences, and (b) the selected images are all
contained under the directory specified as the customary source
root, then the "root of destination" text box will also be
automatically filled out.
For example, suppose that you have specified '/home/user/Staging'
as your customary source root and '/mnt/storage' as your customary
destination root. If all selected images fell under the directory
'/home/user/Staging/2020/Roll0001', the "root of destination" would
be automatically filled out with '/mnt/storage/2020/Roll0001'.
But if all selected images fall under a directory outside the
specified customary source root (e.g., '/opt/other'), the "root
of destination" text box must be filled out manually.
It is also possible to edit the "root of destination" further once
it has been automatically filled out.
4. Click the "move" or "copy" button.
Before moving or copying any images, the module will first
replicate the necessary directory hierarchy by creating all
destination directories that do not already exist; should a
directory creation attempt fail, the operation will be
aborted, but any directories already created will not be
removed.
During the actual move/copy operation, the module transfers an
image by taking its path and replacing the string in the "existing
root" text box with that in the "root of destination" text box
(e.g., '/home/user/Staging/2020/Roll0001/DSC_0001.jpg' would be
transferred to '/mnt/storage/2020/Roll0001/DSC_0001.jpg').
LICENSE
LGPLv2+
]]
-- Header material: BEGIN
local darktable = require("darktable")
local dtutils = require("lib/dtutils")
local dtutils_file = require("lib/dtutils.file")
local dtutils_system = require("lib/dtutils.system")
local LIB_ID = "transfer_hierarchy"
dtutils.check_min_api_version("5.0.0", LIB_ID)
local MKDIR_COMMAND = darktable.configuration.running_os == "windows" and "mkdir " or "mkdir -p "
local PATH_SEPARATOR = darktable.configuration.running_os == "windows" and "\\\\" or "/"
local PATH_SEGMENT_REGEX = "(" .. PATH_SEPARATOR .. "?)([^" .. PATH_SEPARATOR .. "]+)"
unpack = unpack or table.unpack
gmatch = string.gfind or string.gmatch
darktable.gettext.bindtextdomain(LIB_ID, darktable.configuration.config_dir .. PATH_SEPARATOR .. "lua" .. PATH_SEPARATOR .. "locale" .. PATH_SEPARATOR)
local function _(msgid)
return darktable.gettext.dgettext(LIB_ID, msgid)
end
-- Header material: END
-- Helper functions: BEGIN
local function pathExists(path)
local success, err, errno = os.rename(path, path)
if not success then
if errno == 13 then
return true
end
end
return success, err
end
local function pathIsDirectory(path)
return pathExists(path..PATH_SEPARATOR)
end
local function createDirectory(path)
local errorlevel = dtutils_system.external_command(MKDIR_COMMAND .. dtutils_file.sanitize_filename(path))
if errorlevel == 0 and pathIsDirectory(path) then
return path
else
return nil
end
end
-- Helper functions: END
-- Widgets and business logic: BEGIN
local sourceTextBox = darktable.new_widget("entry") {
tooltip = _("Lowest directory containing all selected images"),
editable = false
}
sourceTextBox.reset_callback = function() sourceTextBox.text = "" end
local destinationTextBox = darktable.new_widget("entry") {
text = ""
}
destinationTextBox.reset_callback = function() destinationTextBox.text = "" end
local function findRootPath(films)
local commonSegments = nil
local prefix = ""
for film, _ in pairs(films) do
local path = film.path
if commonSegments == nil then
commonSegments = {}
local firstMatchIndex = string.find(path, PATH_SEGMENT_REGEX)
if firstMatchIndex ~= nil then
prefix = string.sub(path, 1, firstMatchIndex-1)
end
string.gsub(path, PATH_SEGMENT_REGEX, function(w, x)
if w ~= "" then table.insert(commonSegments, w) end
table.insert(commonSegments, x)
end)
else
local matcher = gmatch(path, PATH_SEGMENT_REGEX)
local i = 1
while i < #commonSegments do
match, match2 = matcher()
if match == nil then
while i <= #commonSegments do
table.remove(commonSegments, #commonSegments)
end
break
elseif match ~= "" then
if commonSegments[i] ~= match then
while i <= #commonSegments do
table.remove(commonSegments, #commonSegments)
end
break
else
i = i+1
end
end
if match2 == nil or commonSegments[i] ~= match2 then
while i <= #commonSegments do
table.remove(commonSegments, #commonSegments)
end
break
else
i = i+1
end
end
end
end
if commonSegments == nil then
return prefix
end
if commonSegments[#commonSegments] == PATH_SEPARATOR then
table.remove(commonSegments, #commonSegments)
end
rv = prefix .. table.concat(commonSegments)
return rv
end
local function calculateRoot()
films = {}
for _,img in ipairs(darktable.gui.action_images) do
films[img.film] = true
end
return findRootPath(films), films
end
local function doCalculate()
local rootPath = calculateRoot()
if rootPath ~= nil then
sourceTextBox.text = rootPath
local sourceBase = darktable.preferences.read(LIB_ID, "source_base", "directory")
local destBase = darktable.preferences.read(LIB_ID, "destination_base", "directory")
if sourceBase ~= nil and sourceBase ~= "" and
destBase ~= nil and destBase ~= "" and
string.sub(rootPath, 1, #sourceBase) == sourceBase then
destinationTextBox.text = destBase .. string.sub(rootPath, #sourceBase+1)
end
end
end
local function stopTransfer(transferJob)
transferJob.valid = false
end
local function doTransfer(transferFunc)
rootPath, films = calculateRoot()
if rootPath ~= sourceTextBox.text then
darktable.print(_("transfer hierarchy: ERROR: existing root is out of sync -- click 'calculate' to update"))
return
end
if destinationTextBox.text == "" then
darktable.print(_("transfer hierarchy: ERROR: destination not specified"))
return
end
local sourceBase = sourceTextBox.text
local destBase = destinationTextBox.text
local destFilms = {}
for film, _ in pairs(films) do
films[film] = destBase .. string.sub(film.path, #sourceBase+1)
if not pathExists(films[film]) then
if createDirectory(films[film]) == nil then
darktable.print(_("transfer hierarchy: ERROR: could not create directory: " .. films[film]))
return
end
end
if not pathIsDirectory(films[film]) then
darktable.print(_("transfer hierarchy: ERROR: not a directory: " .. films[film]))
return
end
destFilms[film] = darktable.films.new(films[film])
if destFilms[film] == nil then
darktable.print(_("transfer hierarchy: ERROR: could not create film: " .. film.path))
end
end
local srcFilms = {}
for _,img in ipairs(darktable.gui.action_images) do
srcFilms[img] = img.film
end
local job = darktable.gui.create_job(string.format(_("transfer hierarchy") .. " (%d image" .. (#(darktable.gui.action_images) == 1 and "" or "s") .. ")", #(darktable.gui.action_images)), true, stopTransfer)
job.percent = 0.0
local pctIncrement = 1.0 / #(darktable.gui.action_images)
for _,img in ipairs(darktable.gui.action_images) do
if job.valid and img.film == srcFilms[img] then
destFilm = destFilms[img.film]
transferFunc(img, destFilm)
job.percent = job.percent + pctIncrement
end
end
job.valid = false
local filterRules = darktable.gui.libs.collect.filter()
darktable.gui.libs.collect.filter(filterRules)
end
local function doMove()
doTransfer(darktable.database.move_image)
end
local function doCopy()
doTransfer(darktable.database.copy_image)
end
local transfer_widget = darktable.new_widget("box") {
orientation = "vertical",
darktable.new_widget("button") {
label = _("calculate"),
clicked_callback = doCalculate
},
darktable.new_widget("label") {
label = _("existing root"),
halign = "start"
},
sourceTextBox,
darktable.new_widget("label") {
label = _("root of destination"),
halign = "start"
},
destinationTextBox,
darktable.new_widget("button") {
label = _("move"),
tooltip = "Move all selected images",
clicked_callback = doMove
},
darktable.new_widget("button") {
label = _("copy"),
tooltip = _("Copy all selected images"),
clicked_callback = doCopy
}
}
-- Widgets and business logic: END
-- Preferences: BEGIN
darktable.preferences.register(
LIB_ID,
"source_base",
"string",
"[transfer hierarchy] Customary source root",
"",
"")
darktable.preferences.register(
LIB_ID,
"destination_base",
"string",
"[transfer hierarchy] Customary destination root",
"",
"")
-- Preferences: END
darktable.register_lib(LIB_ID,
"transfer hierarchy", true, true, {
[darktable.gui.views.lighttable] = { "DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 700 }
}, transfer_widget, nil, nil)