I was also frustrated at there not being an option to redirect the automatic save versions to output to another path and the addon I found wasn't very customizable without manual work, so I created my own.
This addon allows setting a custom path for .blend file save versions to be moved to, with support for using environment variables and a few other variables such as the date, a UTC timestamp, the name of the current .blend file, etc.
The addon itself is pretty simple; it simply checks every time the current file is saved for the newest .blend# file with the same name, then copies it to the user-specified path, overwriting the oldest save version in the destination path if it's at the max amount of save versions (which can be set in the base Blender preferences at Edit > Preferences... > Save & Load > Save Versions). It then removes all .blend# files in the original directory to clean up.
As an example, if you have my_file.blend1 and my_file.blend2, it will copy my_file.blend2 (if it actually is the newest) to the path you set in the preferences and then remove my_file.blend1 and the original my_file.blend2.
This happens because I've witnessed a few stragglers that sometimes get left behind when a file is saved rapidly and simply checking for and moving .blend1 didn't seem to be consistent. It could be a logic error on my part, but I personally like the auto-cleanup anyway.
If you'd like to change this behavior to only remove the original .blend save version that was copied, replace the code (on lines 122-123)
for file in unmoved_save_vers:
os.remove(file)
with
os.remove(saved_ver_file_path)
That will leave any other .blend# version files intact, though from my experience that means you'll occasionally have to manually delete some if you save too rapidly.
This is the save path I use myself (and the default), but you're free to change it to whatever you wish in the addon's preferences!
%temp%\Blender Backup Saves\{year}-{month}-{day}\{filename}\
So far I've used the addon with no problems but my python skills are rusty and I'm relatively new to using python with Blender so let me know if you spot or encounter any issues.
custom_save_versions_path_v1.0.0.zip
You can install it like most other Blender addons by going to Edit > Preferences... > Add-ons > Install... and selecting the .zip file, then enabling it. The single setting (to set the save path) is in the same panel.
Here's the entire code for the addon. You can install this manually yourself if you'd like by copy-pasting this into a file named __init__.py and putting it in a folder, then zipping that folder and installing it as an addon via the instructions above.
bl_info = {
"name": "Custom Save Versions Path",
"description": "Allows you to set and use a custom directory for Blender's save version files (.blend1, .blend2, etc).",
"author": "Unordinal",
"version": (1, 0, 0),
"blender": (3, 6, 0),
"location": "",
"warning": "",
"category": "System"
}
import bpy
from bpy.app.handlers import persistent
from bpy.types import Operator, AddonPreferences
from bpy.props import StringProperty
import os
import re
import shutil
from stat import *
import datetime
from datetime import datetime
def get_custom_save_vers_path():
prefs = bpy.context.preferences.addons[name].preferences
current_datetime = datetime.utcnow()
epoch = datetime(1970, 1, 1)
timestamp = (current_datetime - epoch).total_seconds()
current_date = datetime.today()
year = current_date.year
month = current_date.month
day = current_date.day
file_path = bpy.data.filepath
file_directory = os.path.dirname(file_path)
file_name = os.path.splitext(bpy.path.basename(bpy.data.filepath))[0]
final_save_path = str(os.path.expandvars(prefs.save_path)).format(
year=str(year),
month=str(month),
day=str(day),
filedir=file_directory,
filename=file_name,
timestamp=str(int(timestamp))
)
return final_save_path
def get_save_ver_num_to_use(save_vers: list[str], max_save_vers: int):
if not save_vers:
return '1'
base_save_name = os.path.splitext(save_vers[0])[0]
for n in range(1, max_save_vers + 1):
save_ver_name = base_save_name + f".blend{n}"
if not os.path.isfile(save_ver_name):
return str(n)
return get_oldest_file(save_vers)[-1]
def get_save_vers_in_directory(directory: str, blend_file_name: str=None):
if not blend_file_name or len(blend_file_name) == 0:
blend_file_name = ".blend"
save_ver_regex = re.compile(r".*%s\d+" % re.escape(blend_file_name))
return [os.path.join(directory, file) for file in os.listdir(directory) if save_ver_regex.match(file)]
def get_oldest_file(file_list: str) -> str:
return min(file_list, key=os.path.getmtime)
def get_newest_file(file_list: str) -> str:
return max(file_list, key=os.path.getmtime)
def clean_up_old_save_vers(save_vers: list[str], max_save_vers: int):
if not save_vers:
return
save_vers_over_max = len(save_vers) - max_save_vers
for _ in range(save_vers_over_max):
oldest_file = get_oldest_file(save_vers)
os.remove(oldest_file)
save_vers.remove(oldest_file)
@persistent
def save_version_move_handler(saved_file_path):
saved_file_name = os.path.basename(saved_file_path)
custom_save_vers_path = get_custom_save_vers_path()
if not os.path.exists(custom_save_vers_path):
os.makedirs(custom_save_vers_path)
max_save_vers = bpy.context.preferences.filepaths.save_version
save_vers = get_save_vers_in_directory(custom_save_vers_path, saved_file_name)
clean_up_old_save_vers(save_vers, max_save_vers)
if max_save_vers == 0:
return
unmoved_save_vers = get_save_vers_in_directory(os.path.dirname(saved_file_path), saved_file_name) # C:\Users\meee\Blender\Scenes\test.blend1, [...].blend2, etc
saved_ver_file_path = get_newest_file(unmoved_save_vers) if unmoved_save_vers else None
if not saved_ver_file_path:
return
saved_ver_base_path = os.path.join(custom_save_vers_path, saved_file_name)
saved_ver_num_to_use = get_save_ver_num_to_use(save_vers, max_save_vers)
saved_ver_output_path = saved_ver_base_path + saved_ver_num_to_use
# This creates a new file over the max if there's a gap in the numbering after a change by the user to the number of save versions.
# Won't fix since the old save over the max will get cleaned up next time the file is saved anyway.
FAIL_COPY_STR = "Failed to copy save version file '%s' to the destination '%s'" % (saved_ver_file_path, saved_ver_output_path)
try:
dest_save_ver = shutil.copy(saved_ver_file_path, saved_ver_output_path)
os.utime(dest_save_ver)
if not os.path.isfile(dest_save_ver):
print(FAIL_COPY_STR)
return
# The save version file was copied successfully, remove any originals.
for file in unmoved_save_vers:
os.remove(file)
except Exception as err:
print(f"{FAIL_COPY_STR}: {err}")
class CustomSaveVersionsPathPreferences(AddonPreferences):
bl_idname = name
save_path: StringProperty(
name="Save Path",
subtype='DIR_PATH', #https://docs.blender.org/api/current/bpy_types_enum_items/property_subtype_items.html#rna-enum-property-subtype-items
default="%temp%\\Blender Backup Saves\\{year}-{month}-{day}\\{filename}\\",
description="""
The path to move newly-created save versions to.
Year: {year}
Month: {month}
Day: {day}
UTC timestamp: {timestamp}
.blend file directory: {filedir}
.blend file name: {filename}
"""
)
def draw(self, context):
layout = self.layout
layout.label(text="Use 'Preferences > Save & Load > Save Versions' to set how many versions are saved.")
layout.prop(self, "save_path")
def register():
bpy.utils.register_class(CustomSaveVersionsPathPreferences)
bpy.app.handlers.save_post.append(save_version_move_handler)
def unregister():
bpy.utils.unregister_class(CustomSaveVersionsPathPreferences)
bpy.app.handlers.save_post.remove(save_version_move_handler)
if name == "main":
register()