diff --git a/scripts-blender/addons/incremental_autosave/CHANGELOG.MD b/scripts-blender/addons/incremental_autosave/CHANGELOG.MD new file mode 100644 index 00000000..17e3f32c --- /dev/null +++ b/scripts-blender/addons/incremental_autosave/CHANGELOG.MD @@ -0,0 +1,4 @@ +## 1.0.0 - 2023-07-06 + +## DOCUMENTED +- Initial release diff --git a/scripts-blender/addons/incremental_autosave/README.md b/scripts-blender/addons/incremental_autosave/README.md new file mode 100644 index 00000000..79840b18 --- /dev/null +++ b/scripts-blender/addons/incremental_autosave/README.md @@ -0,0 +1,22 @@ +# Incremental Autosave + +This add-on was written to address some shortcomings of Blender's default Autosave functionality. + +### Blender's Shortcomings: +- Blender on Linux by default autosaves to /tmp/, which gets nuked on PC restart. So if your PC crashes, your autosaves are gone. +- This forces you to customize your autosave filepath. But if you do that, and then move your preferences to another OS, the filepath will become invalid and autosaving will cease. +- One autosave per file. If you take a break for a few minutes, you can't return to the state from before your break. +- If you just want to go back to a version more than a few minutes old, you simply can't. +- No autosave for files that weren't saved yet. +- No autosave when switching files. + +### This Add-on's Features: +- Separate filepaths for each OS. Eg., when you're on Linux, you can see and specify a Linux filepath, and when you're on Windows, you can set a separate Windows filepath without overwriting the Linux one. +- Even if your path is invalid, it will still save in the default OS temp folder. +- Incremental autosave per file. Eg., if you configure 30 max saves per file at 3 mins/save, you can go back in 3 minute intervals by up to 90 minutes. +- You can configure it however you want, including infinite saves every minute, if you don't mind a manual cleanup once in a while. +- Autosaves when opening another file while current one is dirty. +- Autosaves files that were never saved as "Unnamed.blend". + +### Installation +Place the `incremental_autosave` folder into your Blender addons folder. If you need help finding this folder, you can ask ChatGPT for it. \ No newline at end of file diff --git a/scripts-blender/addons/incremental_autosave/__init__.py b/scripts-blender/addons/incremental_autosave/__init__.py new file mode 100644 index 00000000..859887e6 --- /dev/null +++ b/scripts-blender/addons/incremental_autosave/__init__.py @@ -0,0 +1,209 @@ +bl_info = { + "name": "Incremental Autosave", + "author": "Demeter Dzadik", + "version": (1, 0, 0), + "blender": (2, 80, 0), + "location": "blender", + "description": "Autosaves in a way where subsequent autosaves don't overwrite previous ones", + "category": "System", +} + +import bpy +from bpy.props import BoolProperty, IntProperty, StringProperty +from bpy.app.handlers import persistent +from datetime import datetime +import os, platform, tempfile + +# Timestamp format for prefixing autosave file names. +TIME_FMT_STR = '%Y_%M_%d_%H-%M-%S' + +# Timestamp of when Blender is launched. Used to avoid creating an autosave when opening Blender. +LAUNCH_TIME = datetime.now() + +def get_addon_prefs(): + user_preferences = bpy.context.preferences + return user_preferences.addons[__name__].preferences + +class IncrementalAutoSavePreferences(bpy.types.AddonPreferences): + bl_idname = __name__ + + save_before_close : BoolProperty(name='Save Before File Open', + description='Save the current file before opening another file', + default=True) + save_interval : IntProperty(name='Save Interval (Minutes)', + description="Number of minutes between each save while the add-on is enabled", + default=5, min=1, max=120, soft_max=30) + + use_native_autosave_path: BoolProperty( + name = "Use Native Autosave Path", + description = "If True, use the autosave path that's part of the regular Blender preferences. If you use the add-on's autosave path, it is a per-OS path, so if you have multiple workstations with multiple operating systems, the add-on can have a separate (and functional) filepath for each of them", + default = True + ) + autosave_path_linux : StringProperty(name='Autosave Path (Linux)', + description='Path to auto save files into', + subtype='FILE_PATH', + default='') + autosave_path_windows : StringProperty(name='Autosave Path (Windows)', + description='Path to auto save files into', + subtype='FILE_PATH', + default='') + autosave_path_mac : StringProperty(name='Autosave Path (Mac)', + description='Path to auto save files into', + subtype='FILE_PATH', + default='') + + @property + def autosave_path_naive(self): + """Return the autosave path that the user wishes existed.""" + if self.use_native_autosave_path: + return bpy.context.preferences.filepaths.temporary_directory + system = platform.system() + if system == "Windows": + return self.autosave_path_windows + elif system == "Linux": + return self.autosave_path_linux + elif system == "Darwin": + return self.autosave_path_mac + + @property + def autosave_path(self): + """Return an autosave path that will always actually exist, no matter how desperate.""" + path = self.autosave_path_naive + if path and os.path.exists(path): + return path + + if bpy.data.filepath: + return os.path.dirname(bpy.data.filepath) + + sys_temp = tempfile.gettempdir() + return sys_temp + + max_save_files : bpy.props.IntProperty(name='Max Backups Per File', + description='Maximum number of backups to save for each file, 0 means unlimited. Otherwise, the oldest file will be deleted after reaching the limit', + default=10, min=0, max=100) + compress_files : bpy.props.BoolProperty(name='Compress Files', + description='Save backups with compression enabled', + default=True) + + def draw(self, context): + layout = self.layout.column() + layout.use_property_decorate = False + layout.use_property_split = True + + layout.prop(context.preferences.filepaths, 'use_auto_save_temporary_files', text="Enable Native Autosave") + if not context.preferences.filepaths.use_auto_save_temporary_files: + layout.label(text=" "*40+ "While the built-in autosave is redundant with the add-on, ") + layout.label(text=" "*40+"disabling it could be bad in case the add-on gets disabled.") + + layout.prop(self, 'use_native_autosave_path') + if bpy.data.filepath == '': + par = os.getcwd() + else: + par = None + abs_path = bpy.path.abspath(self.autosave_path_naive, start=par) + + path_row = layout.row() + if not os.path.exists(abs_path): + path_row.alert = True + if self.use_native_autosave_path: + path_row.prop(context.preferences.filepaths, 'temporary_directory', text="Native Autosave Path") + else: + system = platform.system() + if system == "Windows": + path_row.prop(self, 'autosave_path_windows') + elif system == "Linux": + path_row.prop(self, 'autosave_path_linux') + elif system == "Darwin": + path_row.prop(self, 'autosave_path_mac') + + if path_row.alert: + split = layout.split(factor=0.4) + split.row() + split.label(text='Path not found: '+abs_path, icon='ERROR') + fallback_split = layout.split(factor=0.4) + fallback_split.row() + fallback_split.label(text="Fallback path: " + self.autosave_path) + + layout.separator() + + layout.prop(self, 'save_interval') + layout.prop(self, 'max_save_files') + layout.prop(self, 'save_before_close') + layout.separator() + layout.prop(self, 'compress_files') + +def save_file(): + addon_prefs = get_addon_prefs() + + basename = bpy.data.filepath + if basename == '': + basename = 'Unnamed.blend' + else: + basename = bpy.path.basename(basename) + + try: + save_dir = bpy.path.abspath(addon_prefs.autosave_path) + if not os.path.isdir(save_dir): + os.mkdir(save_dir) + except: + print("Incremental Autosave: Error creating auto save directory.") + return + + # Delete old files, to limit the number of saves. + if addon_prefs.max_save_files > 0: + try: + # As we prefix saved blends with a timestamp, + # `sorted()` puts the oldest prefix at the start of the list. + # This should be quicker than getting system timestamps for each file. + otherfiles = sorted([name for name in os.listdir(save_dir) if name.endswith(basename)]) + if len(otherfiles) >= addon_prefs.max_save_files: + while len(otherfiles) >= addon_prefs.max_save_files: + old_file = os.path.join(save_dir,otherfiles[0]) + os.remove(old_file) + otherfiles.pop(0) + except: + print("Incremental Autosave: Unable to remove old files.") + + # Save the copy. + time = datetime.now() + filename = time.strftime(TIME_FMT_STR) + '_' + basename + backup_file = os.path.join(save_dir,filename) + try: + bpy.ops.wm.save_as_mainfile(filepath=backup_file, copy=True, + compress=addon_prefs.compress_files) + print("Incremental Autosave: Saved file: ", backup_file) + except: + print('Incremental Autosave: Error auto saving file.') + +@persistent +def save_before_close(_dummy=None): + # is_dirty means there are changes that haven't been saved to disk + if bpy.data.is_dirty and get_addon_prefs().save_before_close: + save_file() + +def create_autosave(): + now = datetime.now() + delta = now-LAUNCH_TIME + if delta.seconds < 5: + return get_addon_prefs().save_interval * 60 + + if bpy.data.is_dirty: + save_file() + return get_addon_prefs().save_interval * 60 + +@persistent +def register_autosave_timer(_dummy=None): + bpy.app.timers.register(create_autosave) + +def register(): + bpy.utils.register_class(IncrementalAutoSavePreferences) + bpy.app.timers.register(create_autosave) + bpy.app.handlers.load_pre.append(save_before_close) + bpy.app.handlers.load_post.append(register_autosave_timer) + +def unregister(): + save_before_close() + bpy.app.handlers.load_pre.remove(save_before_close) + bpy.app.handlers.load_post.remove(register_autosave_timer) + bpy.app.timers.unregister(create_autosave) + bpy.utils.unregister_class(IncrementalAutoSavePreferences)