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()

Source:Check-for-Unlinked-Media.fh_lua