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