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
.
3.133.142.2