Spring cleaning—archive unused images

After a while, any long-running project gathers a lot of cruft. For example, texture images that were tried once but were discarded in favor of better ones. This script will help us retain a bit of order by finding all files in a selected directory that are not referenced by our .blend file and packing them into a ZIP archive.

We will take care not to move any .blend files to the ZIP archive (after all, those we normally want to be able to render) nor the ZIP archive itself (to prevent endless recursion). Any file that we archive we subsequently try to remove, and if removing a file leaves an empty directory, we remove that directory as well unless it is the directory our .blend file resides in.

The file manipulation functions are provided by Python's os and os.path modules and ZIP files that can be used both on Windows and open platforms can be manipulated with the use of the zipfile module. The zipfile that we move the unused files to we will name Attic.zip:

import Blender
from os import walk,remove,rmdir,removedirs
import os.path
from zipfile import ZipFile
zipname = 'Attic.zip'

The first challenge is to generate a list of all files in the directory where our .blend file sits. The function listfiles() uses the walk() function from Python's os module to recursively descend into the tree of directories and produces a list of files along the way.

By default, the walk() function traverses the directory tree's depth first that allows us to alter the list of directories on the fly. This feature is used here to remove any directories that start with a dot (highlighted). This isn't necessary for the current and parent directories (represented by .. and. respectively) because walk() already filters them out, but this allows us, for example, to also filter out any .svn directories that we may encounter.

The line containing the yield statement returns the results one file at a time so our function may be used as an iterator. (For more on iterators, refer to the online documentation at http://docs.python.org/reference/simple_stmts.html#yield) We join the filename proper and the path to form a complete filename and normalize it (that is, remove double path separators and the like); although normalizing here isn't strictly necessary because walk() is expected to return any paths in normalized form:

def listfiles(dir):
for root,dirs,files in walk(dir):
for file in files:
if not file.startswith('.'):
yield os.path.normpath(os.path.join(root,file))
for d in dirs:
if d.startswith('.'):

dirs.remove(d)

Before we can compare the list of files our .blend file uses to the list of files present in the directory, we make sure any packed file is unpacked to its original file location. This isn't strictly necessary but ensures that we don't move any files to the archive that are not directly used but do have a copy inside the .blend file:

def run():
Blender.UnpackAll(Blender.UnpackModes.USE_ORIGINAL)

The GetPaths() function from the Blender module produces a list of all files used by the .blend file (except for the .blend file itself). We pass it an absolute argument set to True to retrieve filenames with a full path instead of paths relative to the current directory in order to compare these properly with the list produced by the listfiles() function.

Again, we normalize these filenames as well. The highlighted line shows how we retrieve the absolute path of the current directory by passing the shorthand for the current Bender directory ( // ) to the expandpath() function:

files = [os.path.normpath(f) for f in Blender.GetPaths(absolute=True)]
currentdir = Blender.sys.expandpath('//')

Next we create a ZipFile object in write mode. This will truncate any existing archive with the same name and enables us to add files to the archive. The full name of the archive is constructed by joining the current Blender directory and the name we want to use for the archive. The use of the join() function from the os.path module ensures that we construct the full name in a platform-independent way. We set the debug argument of the ZipFile object to 3 to report anything unusual to the console when creating the archive:

zip = ZipFile(os.path.join(currentdir,zipname),'w')
zip.debug = 3

The removefiles variable will record the names of the files we want to remove after we have constructed the archive. We can only safely remove files and directories after we have created the archive or we might refer to directories that no longer exist.

The archive is constructed by looping over the list of all the files in the current Blender directory and comparing them to the list of files used by our .blend file. Any file with an extension such as .blend or .blend1 is skipped (highlighted) as is the archive itself. The files are added to the ZIP file using the write() method, which accepts as a parameter, the filename with a path relative to the archive (and hence the current directory). That way it is easier to unpack the archive in a new location. Any references to files outside the current directory tree are unaffected by the relpath() function. Any file we add to the archive is marked for removal by adding it to the removefiles list. Finally, we close the archive—an important step because omitting it may leave us with a corrupted archive:

removefiles = []
for f in listfiles(currentdir):
if not (f in files
or os.path.splitext(f)[1].startswith('.blend')
or os.path.basename(f) == zipname):

rf = os.path.relpath(f,currentdir)
zip.write(rf)
removefiles.append(f)
zip.close()

The last task left is to remove the files we moved to the archive. The remove() function from Python's os module will accomplish that but we also want to remove any directory that ends up empty after removing the files. Therefore, for each file we remove we determine the name of its directory. We also check if this directory doesn't point to the current directory because we want to make absolutely sure we do not remove it as this is where our .blend files reside. Although an unlikely scenario, it is possible to open a .blend file in Blender and remove the .blend file itself that might leave an empty directory. If we remove this directory any subsequent (auto) save would fail. The relpath() function will return a dot if the directory passed as its first argument points to the same directory as the directory passed as its second argument. (The samefile() function is more robust and direct but not available on Windows.)

If we made certain we are not referring to the current directory we use the removedirs() function to remove the directory. If the directory is not empty this will fail with an OSError exception (that is, the file we removed was not the last file in the directory), which we ignore. The removedirs() function will also remove all parent directories leading to the directory iff they are empty, which is exactly what we want:

for f in removefiles:
remove(f)
d = os.path.dirname(f)
if os.path.relpath(d,currentdir) != '.':
try:
removedirs(d)
except OSError:
pass
if __name__ == '__main__':
run()

The full code is available as zip.py in attic.blend.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.142.2