New add-on: Incremental Autosave #102

Closed
Demeter Dzadik wants to merge 3 commits from Mets:incremental_autosave into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
3 changed files with 235 additions and 0 deletions

View File

@ -0,0 +1,4 @@
## 1.0.0 - 2023-07-06
## DOCUMENTED
- Initial release

View File

@ -0,0 +1,22 @@
# Incremental Autosave
This add-on was written to address some shortcomings of Blender's default Autosave functionality.
### Blender's Shortcomings:
Review

For better communication, consider focusing the text on the features you are adding to Blender's existing functionality. The point about autosaving on tmp is valid, but overall this list is a bit arbitrary.

For better communication, consider focusing the text on the features you are *adding* to Blender's existing functionality. The point about autosaving on tmp is valid, but overall this list is a bit arbitrary.
- 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.
Review

I understand the humour, but please point people to the Blender manual, not a 3rd party service that requires registration.

I understand the humour, but please point people to the Blender manual, not a 3rd party service that requires registration.

View File

@ -0,0 +1,209 @@
bl_info = {
Review

Consider using a code formatter, for example black, to adhere to PEP8.

Consider using a code formatter, for example black, to adhere to PEP8.
"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
Review

Look up import sorting (alphabetical, system imports first, one line per import, etc).

Look up import sorting (alphabetical, system imports first, one line per import, etc).
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)',
Review

macOS

macOS
description='Path to auto save files into',
subtype='FILE_PATH',
default='')
@property
def autosave_path_naive(self):
Review

native not naive

native not naive
"""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, ")
Review

Avoid ASCII art if possible.

Avoid ASCII art if possible.
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:
Review
Avoid bare exceptions https://www.flake8rules.com/rules/E722.html
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)