add: backup version converter/ backup logic 1.0

This commit is contained in:
h z
2025-04-25 13:00:41 +01:00
parent 35c8934963
commit 84494827ad
15 changed files with 2107 additions and 76 deletions

View File

@@ -0,0 +1,172 @@
"""
Backup Conversion System
========================
This module provides functionality for converting backups between different versions.
Each version converter is implemented in a separate file and registered with the system.
Usage:
------
To convert a backup from one version to another:
```python
from misc.backup_converters import convert_backup
# Convert a backup from version 1.0 to the latest version
convert_backup(backup_dir, target_version=None) # None means latest version
```
Adding a New Converter:
----------------------
1. Create a new file in the backup_converters directory, e.g., v1_to_v2.py
2. Implement a function that takes a backup directory and converts it to the target version
3. Register the converter in this file using the register_converter function
Example:
```python
# In v1_to_v2.py
from misc.backup_converters import register_converter
def convert_v1_to_v2(backup_dir):
# Conversion logic here
return new_version
register_converter("1.0", "2.0", convert_v1_to_v2)
```
"""
import os
import json
import logging
from typing import Dict, Callable, Tuple, Optional, List
logger = logging.getLogger(__name__)
CURRENT_VERSION = "1.0"
_converters: Dict[Tuple[str, str], Callable[[str], str]] = {}
def register_converter(from_version: str, to_version: str, converter: Callable[[str], str]):
"""
Register a converter function for a specific version transition.
Args:
from_version (str): The source version
to_version (str): The target version
converter (callable): A function that takes a backup directory path and returns the new version
"""
_converters[(from_version, to_version)] = converter
logger.info(f"Registered converter from version {from_version} to {to_version}")
def get_backup_version(backup_dir: str) -> str:
"""
Get the version of a backup.
Args:
backup_dir (str): Path to the backup directory
Returns:
str: The version of the backup, or "0.0" if no version is found
"""
version_file = os.path.join(backup_dir, "version.json")
if os.path.exists(version_file):
try:
with open(version_file, 'r') as f:
version_data = json.load(f)
return version_data.get("version", "0.0")
except Exception as e:
logger.error(f"Error reading version file: {e}")
return "0.0"
def set_backup_version(backup_dir: str, version: str):
"""
Set the version of a backup.
Args:
backup_dir (str): Path to the backup directory
version (str): The version to set
"""
version_file = os.path.join(backup_dir, "version.json")
try:
with open(version_file, 'w') as f:
json.dump({"version": version}, f)
except Exception as e:
logger.error(f"Error writing version file: {e}")
def find_conversion_path(from_version: str, to_version: str) -> Optional[List[Tuple[str, str]]]:
"""
Find a path of converters to go from one version to another.
Args:
from_version (str): The source version
to_version (str): The target version
Returns:
list: A list of (from, to) version pairs representing the conversion path,
or None if no path is found
"""
if from_version == to_version:
return []
if (from_version, to_version) in _converters:
return [(from_version, to_version)]
queue = [(from_version, [])]
visited = {from_version}
while queue:
current, path = queue.pop(0)
for (src, dst), _ in _converters.items():
if src == current and dst not in visited:
new_path = path + [(src, dst)]
if dst == to_version:
return new_path
visited.add(dst)
queue.append((dst, new_path))
return None
def convert_backup(backup_dir: str, target_version: Optional[str] = None) -> str:
"""
Convert a backup to the target version.
Args:
backup_dir (str): Path to the backup directory
target_version (str, optional): The target version. If None, converts to the latest version.
Returns:
str: The new version of the backup
Raises:
ValueError: If no conversion path is found
"""
if target_version is None:
target_version = CURRENT_VERSION
current_version = get_backup_version(backup_dir)
if current_version == target_version:
return current_version
path = find_conversion_path(current_version, target_version)
if not path:
raise ValueError(f"No conversion path found from version {current_version} to {target_version}")
for from_ver, to_ver in path:
converter = _converters.get((from_ver, to_ver))
if converter:
logger.info(f"Converting backup from version {from_ver} to {to_ver}")
new_version = converter(backup_dir)
set_backup_version(backup_dir, new_version)
else:
raise ValueError(f"Converter not found for {from_ver} to {to_ver}")
return target_version
import pkgutil
import importlib
for _, name, is_pkg in pkgutil.iter_modules([os.path.dirname(__file__)]):
if not is_pkg and name != "__init__":
importlib.import_module(f"misc.backup_converters.{name}")

View File

@@ -0,0 +1,176 @@
"""
Converter from version 0.0 to version 1.0
=========================================
This module converts backups from version 0.0 (old format without version info)
to version 1.0 (current format with version info).
The old version backup has a simple directory structure with .md files for markdown content,
.mdmeta files for markdown metadata, and .meta files for path metadata.
The current version backup has a more complex structure with tree/ and template/ directories,
.json files for markdown content, .json.meta files for path metadata, and a webhook.json file.
For all old version markdowns, they are considered to be using the default template with no other settings,
and their content is converted from plain text to {"markdown": content_in_md}.
For all old version paths, they are considered to have no settings.
"""
import os
import json
import shutil
from datetime import datetime
import logging
import tempfile
from misc.backup_converters import register_converter
logger = logging.getLogger(__name__)
def convert_v0_to_v1(backup_dir: str) -> str:
"""
Convert a backup from version 0.0 to version 1.0.
Args:
backup_dir (str): Path to the backup directory
Returns:
str: The new version ("1.0")
"""
logger.info(f"Converting backup from version 0.0 to 1.0: {backup_dir}")
temp_dir = tempfile.mkdtemp()
new_backup_dir = os.path.join(temp_dir, "new_backup")
os.makedirs(new_backup_dir)
try:
tree_dir = os.path.join(new_backup_dir, 'tree')
template_dir = os.path.join(new_backup_dir, 'template')
os.makedirs(tree_dir)
os.makedirs(template_dir)
with open(os.path.join(new_backup_dir, 'webhook.json'), 'w') as f:
json.dump([], f)
with open(os.path.join(new_backup_dir, 'version.json'), 'w') as f:
json.dump({"version": "1.0"}, f)
root_dir = os.path.join(backup_dir, 'Root')
if os.path.exists(root_dir) and os.path.isdir(root_dir):
source_dir = root_dir
else:
source_dir = backup_dir
convert_directory(source_dir, tree_dir)
for item in os.listdir(new_backup_dir):
src = os.path.join(new_backup_dir, item)
dst = os.path.join(backup_dir, item)
if os.path.isdir(src):
if os.path.exists(dst):
shutil.rmtree(dst)
shutil.copytree(src, dst)
else:
shutil.copy2(src, dst)
shutil.rmtree(temp_dir)
return "1.0"
except Exception as e:
logger.error(f"Error converting backup: {e}")
shutil.rmtree(temp_dir)
raise
def convert_directory(old_dir: str, new_dir: str):
"""
Recursively convert a directory from the old backup format to the new format.
This function processes a directory from the old backup format and converts it to the new format.
It creates a .json.meta file for the directory with metadata from the .meta file (if it exists),
processes all markdown files (.md) in the directory, converting them to .json files with the new format,
and recursively processes all subdirectories.
For markdown files, the function:
1. Reads the content from the .md file
2. Converts the content to the new format {"markdown": content_in_md}
3. Reads metadata from the .mdmeta file (if it exists)
4. Creates a new .json file with the content and metadata
For directories, the function:
1. Reads metadata from the .meta file (if it exists)
2. Creates a new .json.meta file with the metadata
3. Recursively processes all subdirectories
Args:
old_dir (str): Path to the old directory
new_dir (str): Path to the new directory where the converted files will be stored
"""
order = ''
meta_file_path = os.path.join(old_dir, '.meta')
if os.path.exists(meta_file_path):
with open(meta_file_path, 'r') as f:
for line in f:
try:
if line.startswith('order:'):
order = line.strip().split(': ', 1)[1]
except (ValueError, IndexError):
continue
path_meta = {
'name': os.path.basename(old_dir),
'order': order,
'backup_id': 0
}
with open(os.path.join(new_dir, '.json.meta'), 'w') as f:
json.dump(path_meta, f, default=str, indent=2)
for file_name in os.listdir(old_dir):
old_file_path = os.path.join(old_dir, file_name)
if file_name.endswith('.md'):
md_title = file_name[:-3]
with open(old_file_path, 'r', encoding='utf-8') as f:
content = f.read()
new_content = json.dumps({'markdown': content})
created_at = datetime.now()
order = ''
shortcut = ''
mdmeta_path = old_file_path + 'meta'
if os.path.exists(mdmeta_path):
with open(mdmeta_path, 'r') as f:
for line in f:
try:
key, value = line.strip().split(': ', 1)
if key == 'created_at':
created_at = value
elif key == 'order':
order = value
elif key == 'shortcut':
shortcut = value
except ValueError:
continue
md_data = {
'title': md_title,
'content': new_content,
'created_at': created_at,
'order': order,
'shortcut': shortcut,
'backup_id': 0
}
with open(os.path.join(new_dir, f'{md_title}.json'), 'w') as f:
json.dump(md_data, f, default=str, indent=2)
elif os.path.isdir(old_file_path) and not file_name.startswith('.'):
new_subdir = os.path.join(new_dir, file_name)
os.makedirs(new_subdir, exist_ok=True)
convert_directory(old_file_path, new_subdir)
register_converter("0.0", "1.0", convert_v0_to_v1)