From 8923aa7a8bbebbd4b4db7143db96cba78894c767 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Thu, 6 Jul 2023 16:11:19 +0200 Subject: [PATCH 1/3] New add-on: Incremental Autosave --- .../addons/incremental_autosave/CHANGELOG.MD | 4 + .../addons/incremental_autosave/README.md | 22 ++ .../addons/incremental_autosave/__init__.py | 207 ++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 scripts-blender/addons/incremental_autosave/CHANGELOG.MD create mode 100644 scripts-blender/addons/incremental_autosave/README.md create mode 100644 scripts-blender/addons/incremental_autosave/__init__.py 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..4a2de058 --- /dev/null +++ b/scripts-blender/addons/incremental_autosave/__init__.py @@ -0,0 +1,207 @@ +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. (As long as the add-on is enabled, it always auto-saves, since that's all the add-on does)", + 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 Save Files', + description='Maximum number of copies to save, 0 means unlimited', + 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,'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) -- 2.30.2 From 5986af27243aae1de134c27a57dea645b75bb19a Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Thu, 6 Jul 2023 16:15:03 +0200 Subject: [PATCH 2/3] Expose save_before_close option in UI --- scripts-blender/addons/incremental_autosave/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts-blender/addons/incremental_autosave/__init__.py b/scripts-blender/addons/incremental_autosave/__init__.py index 4a2de058..078a7cd7 100644 --- a/scripts-blender/addons/incremental_autosave/__init__.py +++ b/scripts-blender/addons/incremental_autosave/__init__.py @@ -126,9 +126,10 @@ class IncrementalAutoSavePreferences(bpy.types.AddonPreferences): layout.separator() - layout.prop(self,'save_interval') - layout.prop(self,'max_save_files') - layout.prop(self,'compress_files') + layout.prop(self, 'save_interval') + layout.prop(self, 'max_save_files') + layout.prop(self, 'compress_files') + layout.prop(self, 'save_before_close') def save_file(): addon_prefs = get_addon_prefs() -- 2.30.2 From 5a7d92c10711b92d9a1c28ed0440b24ed5488fd7 Mon Sep 17 00:00:00 2001 From: "demeterdzadik@gmail.com" Date: Fri, 7 Jul 2023 11:49:23 +0200 Subject: [PATCH 3/3] Improve UI strings --- scripts-blender/addons/incremental_autosave/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts-blender/addons/incremental_autosave/__init__.py b/scripts-blender/addons/incremental_autosave/__init__.py index 078a7cd7..859887e6 100644 --- a/scripts-blender/addons/incremental_autosave/__init__.py +++ b/scripts-blender/addons/incremental_autosave/__init__.py @@ -31,7 +31,7 @@ class IncrementalAutoSavePreferences(bpy.types.AddonPreferences): 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. (As long as the add-on is enabled, it always auto-saves, since that's all the add-on does)", + 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( @@ -78,8 +78,8 @@ class IncrementalAutoSavePreferences(bpy.types.AddonPreferences): sys_temp = tempfile.gettempdir() return sys_temp - max_save_files : bpy.props.IntProperty(name='Max Save Files', - description='Maximum number of copies to save, 0 means unlimited', + 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', @@ -128,8 +128,9 @@ class IncrementalAutoSavePreferences(bpy.types.AddonPreferences): layout.prop(self, 'save_interval') layout.prop(self, 'max_save_files') - layout.prop(self, 'compress_files') layout.prop(self, 'save_before_close') + layout.separator() + layout.prop(self, 'compress_files') def save_file(): addon_prefs = get_addon_prefs() -- 2.30.2