Check for Unlinked Media.fh_lua--[[
@Title: Check for Unlinked Media
@Author: Jane Taubman
@Version: 2.0
@LastUpdated: August 2017
@Description: Checks all the media files in the media folder are linked to media records the Project and optionally deletes unlinked files.
V1.2 Added options for Moving or just listing files
V1.3 Handles problems where users have the media incorrectly linked to the media folder (e.g using full path names)
V1.4 Correct Issue where Project has special characters in the name,
V1.5 Handle locally linked files with in the Media Folder with out Media Records.
V1.6 Add more robust dirtree function (to handle Mac Directories with invalid windows characters in filenames.
V1.7 Use newer dirtree version
V1.8 Fix handling of absolute full path names
V1.9 Exclude thumbs.db and picassi.ini from list.
V2.0 Lua 5.3 Compatible
V2.1 V7 Compatibility
]]
require 'lfs'
require 'iuplua'
iup.SetGlobal("CUSTOMQUITMESSAGE","YES")
function mainfunction()
if fhGetContextInfo('CI_APP_MODE') ~= 'Project Mode' then
fhMessageBox('This Plugin Requires a Project','MB_OK','MB_ICONEXCLAMATION')
return
end
-- Prompt User for Action
local tactions = {'list','delete','move'}
local intButton = iupButtons("Check for Unlinked Media","If using Move or Delete make sure you have a full backup of your Project, \nBEFORE running. \n\nPlease Select from the following options","V","List Unlinked Media","Delete Unlinked Media","Move Unlinked Media")
local action = tactions[intButton]
if action == nil then
return
end
-- Load Media and File Lists
local filelist = buildfilelist()
local medialist = buildmedialist()
local medialist = addlocalmedialist(medialist)
-- Remove Linked files from File List
for k,v in pairs(medialist) do
filelist[k] = nil
end
-- List remaining files
tblOutput,iC = createResultTable()
-- Define Columns
tblOutput.file = {title='File',type='text',width=540,align='align_left',content={}}
for k,v in pairs(filelist) do
-- Add Columns
iC = iC + 1
tblOutput.file.content[iC] = v
end
if iC > 0 then
-- Offer to delete the files
local strTitle = "Files in Media Folder and Not Linked to Project"
if action == 'delete' then
local a = fhMessageBox('Please confirm the delete of the '..iC..' unlinked files\n\n\nWarning This CAN NOT be undone', "MB_YESNO","MB_ICONEXCLAMATION")
if a == "Yes" then
strTitle = "Deleted Files in Media Folder and Not Linked to Project"
deleteFiles(filelist)
fhMessageBox(iC.." Files Deleted")
end
end
if action == 'move' then
dir = getOutPutDir()
if dir ~= nil then
local a = fhMessageBox('Please confirm the move of the '..iC..' unlinked files to\n'..dir..'\n\nWarning This CAN NOT be undone', "MB_YESNO","MB_ICONEXCLAMATION")
if a == "Yes" then
strTitle = "Moved Files in Media Folder to "..dir
copyFiles(filelist,dir)
deleteFiles(filelist)
fhMessageBox(iC.." Files Moved to "..dir)
fhShellExecute(dir)
end
end
end
fhOutputResultSetTitles(strTitle,strTitle, "Date: %#x")
for t in tblOutput() do
fhOutputResultSetColumn(t.title, t.type, t.content, iC, t.width,t.align)
end
else
fhMessageBox('No Unlinked Files Found','MB_OK','MB_ICONINFORMATION')
end
end
-------------------------------------- Custom Functions
function buildfilelist()
local rootfolder = fhGetContextInfo('CI_PROJECT_DATA_FOLDER')
local mediafolder = rootfolder..'\\media'
local rootpattern = strPlainText(rootfolder)
local filelist = {}
for filename,attr in dirtree(mediafolder) do
if type(attr) == 'table' then
if attr.mode == 'file' then
local strlc = string.lower(filename:gsub(rootpattern..'\\',''))
local path,file,ext = SplitFilename(strlc)
if file ~= 'thumbs.db' and file ~='.picasa.ini' then
filelist[strlc] = filename
end
end
end
end
return filelist
end
function buildmedialist()
local rootfolder = fhGetContextInfo('CI_PROJECT_DATA_FOLDER')
local mediafolder = rootfolder..'\\'
mediafolder = strPlainText(mediafolder:lower())
local medialist = {}
local pm = fhNewItemPtr()
for pi in records('OBJE') do
pm:MoveTo(pi,'~._FILE')
local mediaFile = fhGetValueAsText(pm)
local strlc = mediaFile:lower()
-- Trap for Media in the Media folder where people have done the linking wrong
strlc = strlc:gsub(mediafolder,'')
medialist[strlc] = fhGetValueAsText(pm)
end
return medialist
end
function addlocalmedialist(medialist)
local rootfolder = fhGetContextInfo('CI_PROJECT_DATA_FOLDER')
local mediafolder = rootfolder..'\\'
mediafolder = strPlainText(mediafolder:lower())
local strItemTag = 'FILE'
local iCount = fhGetRecordTypeCount() -- Get Count of Record types
-- Loop through Record Types
local ii = 0
local ptr = fhNewItemPtr()
for ii =1,iCount do
strRecType = fhGetRecordTypeTag(ii)
ptr:MoveToFirstRecord(strRecType)
while ptr:IsNotNull() do
strPtrTag = fhGetTag(ptr)
if strPtrTag == strItemTag then
local mediaFile = fhGetValueAsText(ptr)
local strlc = mediaFile:lower()
strlc = strlc:gsub(mediafolder,'')
medialist[strlc] = mediaFile
end
ptr:MoveNextSpecial()
end
end
return medialist
end
function deleteFiles(filelist)
for k,v in pairs(filelist) do
os.remove(v)
end
end
function copyFiles(filelist,dir)
local rootfolder = fhGetContextInfo('CI_PROJECT_DATA_FOLDER')
local mediafolder = rootfolder..'\\media\\'
mediafolder = strPlainText(mediafolder)
for k,v in pairs(filelist) do
local newFile = v:gsub(mediafolder,'')
local newFile = dir..'\\'..newFile:gsub('\\','-')
CopyFile(v,newFile,true)
end
end
function getOutPutDir()
local dir = fhGetContextInfo('CI_PROJECT_PUBLIC_FOLDER')
filedlg = iup.filedlg{dialogtype = "DIR", title = "Please select destination directory", DIRECTORY=dir}
-- Shows file dialog in the center of the screen
filedlg:popup (iup.ANYWHERE, iup.ANYWHERE)
-- Gets file dialog status
status = filedlg.status
if status == "-1" then
iup.Message("IupFileDlg","Operation canceled")
return nil
end
return filedlg.value
end
-------------------------------------- Standard Functions
function strPlainText(strText)
-- Prefix every non-alphanumeric character (%W) with a % escape character, where %% is the % escape, and %1 is original character
return strText:gsub("(%W)","%%%1")
end
function CopyFile(strfromfile,strtofile,bReplace)
if not(bReplace) then
if file_exists(strtofile) then
return false
end
end
local inp = assert(io.open(strfromfile, "rb"))
local out = assert(io.open(strtofile, "wb"))
local data = inp:read("*all")
out:write(data)
assert(inp:close())
assert(out:close())
-- Copy the last modification date and access date from the original.
local attr = lfs.attributes(strfromfile)
lfs.touch(strtofile,attr['modification'],attr['access'])
return true
end
function dirtree(dir)
assert(dir and dir ~= "", "directory parameter is missing or empty")
if string.sub(dir, -1) == "\\" then
dir=string.sub(dir, 1, -2)
end
local function yieldtree(dir)
for entry in lfs.dir(dir) do
if entry ~= "." and entry ~= ".." then
entry=dir.."\\"..entry
local attr=lfs.attributes(entry)
coroutine.yield(entry,attr)
if type(attr)=='table' and attr.mode == "directory" then
yieldtree(entry)
end
end
end
end
return coroutine.wrap(function() yieldtree(dir) end)
end
function records(type)
local pi = fhNewItemPtr()
pi:MoveToFirstRecord(type)
return function ()
p2 = pi:Clone()
pi:MoveNext()
if p2:IsNotNull() then return p2:Clone() end
end
end
function createResultTable()
-- create metatable
local tblOutput_mt = {}
tblOutput_mt.col = 0
tblOutput_mt.seq = {}
tblOutput_mt.__newindex = function (t,k,v)
rawset(t,k,v) -- update original table
local m = getmetatable(t)
m.col = m.col + 1
table.insert(m.seq,k)
end
tblOutput_mt.__call = function (t)
local i = 0
local m = getmetatable(t)
local n = table.getn(m.seq)
return function ()
i = i + 1
if i <= n then return t[m.seq[i]] end
end
end
local tblOutput = {} -- Define Columns Table
setmetatable(tblOutput, tblOutput_mt)
local iC = 0 -- Define count of lines
return tblOutput,iC
end
function iupButtons(strTitle,strMessage,strBoxType,...)
arg = {...}
arg['n'] = #arg
local intButton = 0 -- Returned value if X Close button is used
-- Create the GUI labels and buttons
local lblMessage = iup.label{title=strMessage,expand="YES"}
local lblLineSep = iup.label{separator="HORIZONTAL"}
local iupBox = iup.hbox{homogeneous="YES"}
if strBoxType == "V" then
iupBox = iup.vbox{homogeneous="YES"}
end
for intArgNum, strButton in ipairs(arg) do
local btnName = iup.button{title=strButton,expand="YES",padding="4",action=
function () intButton=intArgNum return iup.CLOSE end
}
iup.Append(iupBox,btnName)
end
-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
local dialogue = iup.dialog{title=strTitle,iup.vbox{lblMessage,lblLineSep,iupBox},dialogframe="YES",background="250 250 250",gap="8",margin="8x8"}
dialogue:show()
if (iup.MainLoopLevel()==0) then iup.MainLoop() end
dialogue:destroy()
return intButton
end -- iupButtons
function SplitFilename(strFilename)
-- Returns the Path, Filename, and Extension as 3 values
return string.match(strFilename, "(.-)([^\\]-([^\\%.]+))$")
end
if fhGetAppVersion() > 6 then
function table.getn(t)
local count = 0
for _, __ in pairs(t) do
count = count + 1
end
return count
end
function unpack(t)
return table.unpack(t)
end
end
-------------------------------------- Call Main Function
mainfunction()
--[[
@Title: Check for Unlinked Media
@Author: Jane Taubman
@Version: 2.0
@LastUpdated: August 2017
@Description: Checks all the media files in the media folder are linked to media records the Project and optionally deletes unlinked files.
V1.2 Added options for Moving or just listing files
V1.3 Handles problems where users have the media incorrectly linked to the media folder (e.g using full path names)
V1.4 Correct Issue where Project has special characters in the name,
V1.5 Handle locally linked files with in the Media Folder with out Media Records.
V1.6 Add more robust dirtree function (to handle Mac Directories with invalid windows characters in filenames.
V1.7 Use newer dirtree version
V1.8 Fix handling of absolute full path names
V1.9 Exclude thumbs.db and picassi.ini from list.
V2.0 Lua 5.3 Compatible
V2.1 V7 Compatibility
]]
require 'lfs'
require 'iuplua'
iup.SetGlobal("CUSTOMQUITMESSAGE","YES")
function mainfunction()
if fhGetContextInfo('CI_APP_MODE') ~= 'Project Mode' then
fhMessageBox('This Plugin Requires a Project','MB_OK','MB_ICONEXCLAMATION')
return
end
-- Prompt User for Action
local tactions = {'list','delete','move'}
local intButton = iupButtons("Check for Unlinked Media","If using Move or Delete make sure you have a full backup of your Project, \nBEFORE running. \n\nPlease Select from the following options","V","List Unlinked Media","Delete Unlinked Media","Move Unlinked Media")
local action = tactions[intButton]
if action == nil then
return
end
-- Load Media and File Lists
local filelist = buildfilelist()
local medialist = buildmedialist()
local medialist = addlocalmedialist(medialist)
-- Remove Linked files from File List
for k,v in pairs(medialist) do
filelist[k] = nil
end
-- List remaining files
tblOutput,iC = createResultTable()
-- Define Columns
tblOutput.file = {title='File',type='text',width=540,align='align_left',content={}}
for k,v in pairs(filelist) do
-- Add Columns
iC = iC + 1
tblOutput.file.content[iC] = v
end
if iC > 0 then
-- Offer to delete the files
local strTitle = "Files in Media Folder and Not Linked to Project"
if action == 'delete' then
local a = fhMessageBox('Please confirm the delete of the '..iC..' unlinked files\n\n\nWarning This CAN NOT be undone', "MB_YESNO","MB_ICONEXCLAMATION")
if a == "Yes" then
strTitle = "Deleted Files in Media Folder and Not Linked to Project"
deleteFiles(filelist)
fhMessageBox(iC.." Files Deleted")
end
end
if action == 'move' then
dir = getOutPutDir()
if dir ~= nil then
local a = fhMessageBox('Please confirm the move of the '..iC..' unlinked files to\n'..dir..'\n\nWarning This CAN NOT be undone', "MB_YESNO","MB_ICONEXCLAMATION")
if a == "Yes" then
strTitle = "Moved Files in Media Folder to "..dir
copyFiles(filelist,dir)
deleteFiles(filelist)
fhMessageBox(iC.." Files Moved to "..dir)
fhShellExecute(dir)
end
end
end
fhOutputResultSetTitles(strTitle,strTitle, "Date: %#x")
for t in tblOutput() do
fhOutputResultSetColumn(t.title, t.type, t.content, iC, t.width,t.align)
end
else
fhMessageBox('No Unlinked Files Found','MB_OK','MB_ICONINFORMATION')
end
end
-------------------------------------- Custom Functions
function buildfilelist()
local rootfolder = fhGetContextInfo('CI_PROJECT_DATA_FOLDER')
local mediafolder = rootfolder..'\\media'
local rootpattern = strPlainText(rootfolder)
local filelist = {}
for filename,attr in dirtree(mediafolder) do
if type(attr) == 'table' then
if attr.mode == 'file' then
local strlc = string.lower(filename:gsub(rootpattern..'\\',''))
local path,file,ext = SplitFilename(strlc)
if file ~= 'thumbs.db' and file ~='.picasa.ini' then
filelist[strlc] = filename
end
end
end
end
return filelist
end
function buildmedialist()
local rootfolder = fhGetContextInfo('CI_PROJECT_DATA_FOLDER')
local mediafolder = rootfolder..'\\'
mediafolder = strPlainText(mediafolder:lower())
local medialist = {}
local pm = fhNewItemPtr()
for pi in records('OBJE') do
pm:MoveTo(pi,'~._FILE')
local mediaFile = fhGetValueAsText(pm)
local strlc = mediaFile:lower()
-- Trap for Media in the Media folder where people have done the linking wrong
strlc = strlc:gsub(mediafolder,'')
medialist[strlc] = fhGetValueAsText(pm)
end
return medialist
end
function addlocalmedialist(medialist)
local rootfolder = fhGetContextInfo('CI_PROJECT_DATA_FOLDER')
local mediafolder = rootfolder..'\\'
mediafolder = strPlainText(mediafolder:lower())
local strItemTag = 'FILE'
local iCount = fhGetRecordTypeCount() -- Get Count of Record types
-- Loop through Record Types
local ii = 0
local ptr = fhNewItemPtr()
for ii =1,iCount do
strRecType = fhGetRecordTypeTag(ii)
ptr:MoveToFirstRecord(strRecType)
while ptr:IsNotNull() do
strPtrTag = fhGetTag(ptr)
if strPtrTag == strItemTag then
local mediaFile = fhGetValueAsText(ptr)
local strlc = mediaFile:lower()
strlc = strlc:gsub(mediafolder,'')
medialist[strlc] = mediaFile
end
ptr:MoveNextSpecial()
end
end
return medialist
end
function deleteFiles(filelist)
for k,v in pairs(filelist) do
os.remove(v)
end
end
function copyFiles(filelist,dir)
local rootfolder = fhGetContextInfo('CI_PROJECT_DATA_FOLDER')
local mediafolder = rootfolder..'\\media\\'
mediafolder = strPlainText(mediafolder)
for k,v in pairs(filelist) do
local newFile = v:gsub(mediafolder,'')
local newFile = dir..'\\'..newFile:gsub('\\','-')
CopyFile(v,newFile,true)
end
end
function getOutPutDir()
local dir = fhGetContextInfo('CI_PROJECT_PUBLIC_FOLDER')
filedlg = iup.filedlg{dialogtype = "DIR", title = "Please select destination directory", DIRECTORY=dir}
-- Shows file dialog in the center of the screen
filedlg:popup (iup.ANYWHERE, iup.ANYWHERE)
-- Gets file dialog status
status = filedlg.status
if status == "-1" then
iup.Message("IupFileDlg","Operation canceled")
return nil
end
return filedlg.value
end
-------------------------------------- Standard Functions
function strPlainText(strText)
-- Prefix every non-alphanumeric character (%W) with a % escape character, where %% is the % escape, and %1 is original character
return strText:gsub("(%W)","%%%1")
end
function CopyFile(strfromfile,strtofile,bReplace)
if not(bReplace) then
if file_exists(strtofile) then
return false
end
end
local inp = assert(io.open(strfromfile, "rb"))
local out = assert(io.open(strtofile, "wb"))
local data = inp:read("*all")
out:write(data)
assert(inp:close())
assert(out:close())
-- Copy the last modification date and access date from the original.
local attr = lfs.attributes(strfromfile)
lfs.touch(strtofile,attr['modification'],attr['access'])
return true
end
function dirtree(dir)
assert(dir and dir ~= "", "directory parameter is missing or empty")
if string.sub(dir, -1) == "\\" then
dir=string.sub(dir, 1, -2)
end
local function yieldtree(dir)
for entry in lfs.dir(dir) do
if entry ~= "." and entry ~= ".." then
entry=dir.."\\"..entry
local attr=lfs.attributes(entry)
coroutine.yield(entry,attr)
if type(attr)=='table' and attr.mode == "directory" then
yieldtree(entry)
end
end
end
end
return coroutine.wrap(function() yieldtree(dir) end)
end
function records(type)
local pi = fhNewItemPtr()
pi:MoveToFirstRecord(type)
return function ()
p2 = pi:Clone()
pi:MoveNext()
if p2:IsNotNull() then return p2:Clone() end
end
end
function createResultTable()
-- create metatable
local tblOutput_mt = {}
tblOutput_mt.col = 0
tblOutput_mt.seq = {}
tblOutput_mt.__newindex = function (t,k,v)
rawset(t,k,v) -- update original table
local m = getmetatable(t)
m.col = m.col + 1
table.insert(m.seq,k)
end
tblOutput_mt.__call = function (t)
local i = 0
local m = getmetatable(t)
local n = table.getn(m.seq)
return function ()
i = i + 1
if i <= n then return t[m.seq[i]] end
end
end
local tblOutput = {} -- Define Columns Table
setmetatable(tblOutput, tblOutput_mt)
local iC = 0 -- Define count of lines
return tblOutput,iC
end
function iupButtons(strTitle,strMessage,strBoxType,...)
arg = {...}
arg['n'] = #arg
local intButton = 0 -- Returned value if X Close button is used
-- Create the GUI labels and buttons
local lblMessage = iup.label{title=strMessage,expand="YES"}
local lblLineSep = iup.label{separator="HORIZONTAL"}
local iupBox = iup.hbox{homogeneous="YES"}
if strBoxType == "V" then
iupBox = iup.vbox{homogeneous="YES"}
end
for intArgNum, strButton in ipairs(arg) do
local btnName = iup.button{title=strButton,expand="YES",padding="4",action=
function () intButton=intArgNum return iup.CLOSE end
}
iup.Append(iupBox,btnName)
end
-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
local dialogue = iup.dialog{title=strTitle,iup.vbox{lblMessage,lblLineSep,iupBox},dialogframe="YES",background="250 250 250",gap="8",margin="8x8"}
dialogue:show()
if (iup.MainLoopLevel()==0) then iup.MainLoop() end
dialogue:destroy()
return intButton
end -- iupButtons
function SplitFilename(strFilename)
-- Returns the Path, Filename, and Extension as 3 values
return string.match(strFilename, "(.-)([^\\]-([^\\%.]+))$")
end
if fhGetAppVersion() > 6 then
function table.getn(t)
local count = 0
for _, __ in pairs(t) do
count = count + 1
end
return count
end
function unpack(t)
return table.unpack(t)
end
end
-------------------------------------- Call Main Function
mainfunction()Source:Check-for-Unlinked-Media.fh_lua