mirror of
https://git.srhtnin.garden/sarah/SaraSort.git
synced 2026-10-14 02:38:12 +02:00
Export and import all of SaraSortd's configs
Settings > SaraSortd can export GlobalConf.toml, both templates and every directory's config as one file, and import it again. An import backs up the current configs first; directories that don't exist can be given a new path (InputDir/OutputDir, ValidInputDirs and the config's own name follow) or made. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e90f59bc16
commit
60da6dd9ab
4 files changed
+337
-8
No files matched your search
@@ -29,7 +29,11 @@ Needs SaraSortd **7.1** or newer for arrows with their own patterns (per-entry `
|
|||||||
|
|
||||||
The gear button opens **Settings**, laid out like SaraFem's and SaraSuite's: the shared SaraSuite
|
The gear button opens **Settings**, laid out like SaraFem's and SaraSuite's: the shared SaraSuite
|
||||||
appearance and export/import (same file format as SaraFem's, `[SaraSuite]` plus `[SaraSort]`), SaraSort's
|
appearance and export/import (same file format as SaraFem's, `[SaraSuite]` plus `[SaraSort]`), SaraSort's
|
||||||
own settings, and where SaraSortd lives. Run as an AppImage, the header also has an "Add to app menu"
|
own settings, and where SaraSortd lives. The SaraSortd tab also exports and imports all of SaraSortd's
|
||||||
|
configs as one file: GlobalConf.toml, both templates and every directory's own config, comments included.
|
||||||
|
Importing backs the current configs up to `~/.config/SaraSuite/SaraSort/Backups/` first. If directories it
|
||||||
|
uses don't exist, you can cancel, give any of them a new path (the configs and arrows follow), or have them
|
||||||
|
made. Run as an AppImage, the header also has an "Add to app menu"
|
||||||
button, like SaraSuite's.
|
button, like SaraSuite's.
|
||||||
|
|
||||||
After a change to `GlobalConf.toml` (new inputs or outputs) SaraSort restarts SaraSortd, since SaraSortd
|
After a change to `GlobalConf.toml` (new inputs or outputs) SaraSort restarts SaraSortd, since SaraSortd
|
||||||
@@ -44,6 +48,7 @@ only reads that file when it starts. That can be turned off in Settings.
|
|||||||
| `~/.config/SaraSuite/SaraSort/Config.toml` | SaraSort's own settings |
|
| `~/.config/SaraSuite/SaraSort/Config.toml` | SaraSort's own settings |
|
||||||
| `~/.config/SaraSuite/SaraSort/Layout.toml` | where flowchart boxes were dragged to |
|
| `~/.config/SaraSuite/SaraSort/Layout.toml` | where flowchart boxes were dragged to |
|
||||||
| `~/.config/SaraSuite/SaraSort/Templates/` | your own templates |
|
| `~/.config/SaraSuite/SaraSort/Templates/` | your own templates |
|
||||||
|
| `~/.config/SaraSuite/SaraSort/Backups/` | SaraSortd's configs from before each import |
|
||||||
| `~/.config/SaraSuite/SaraSortd/` | SaraSortd's configs, as SaraSuite installs them (changeable in Settings) |
|
| `~/.config/SaraSuite/SaraSortd/` | SaraSortd's configs, as SaraSuite installs them (changeable in Settings) |
|
||||||
|
|
||||||
## Running and building
|
## Running and building
|
||||||
@@ -64,5 +69,6 @@ only reads that file when it starts. That can be turned off in Settings.
|
|||||||
| `Service.py` | systemctl and journalctl |
|
| `Service.py` | systemctl and journalctl |
|
||||||
| `Templates.py` | built-in and your own templates |
|
| `Templates.py` | built-in and your own templates |
|
||||||
| `Settings.py` | the Settings dialog |
|
| `Settings.py` | the Settings dialog |
|
||||||
|
| `SortdTransfer.py` | exporting/importing all of SaraSortd's configs |
|
||||||
| `Widgets.py` | the shared App context and small controls |
|
| `Widgets.py` | the shared App context and small controls |
|
||||||
| `*Page.py` | one per page |
|
| `*Page.py` | one per page |
|
||||||
+108
-1
@@ -14,7 +14,9 @@ from Core import (
|
|||||||
AppConfig, AvailableThemes, BundledFontFamily, ChildEnvironment, ChipButtonStyle, ExportSettings, Home,
|
AppConfig, AvailableThemes, BundledFontFamily, ChildEnvironment, ChipButtonStyle, ExportSettings, Home,
|
||||||
ImportSettings, LoadSharedSettings, Palette, SharedSettings,
|
ImportSettings, LoadSharedSettings, Palette, SharedSettings,
|
||||||
)
|
)
|
||||||
from Widgets import App
|
from Sortd import MajorVersion, Setup, SortdError
|
||||||
|
from SortdTransfer import ApplyBundle, Bundle, BundleDirectories, ExportSortd, MissingDirectories, ReadBundleSafely
|
||||||
|
from Widgets import App, DirField, ExpandPath
|
||||||
|
|
||||||
DialogWidth = 700
|
DialogWidth = 700
|
||||||
DialogHeight = 520
|
DialogHeight = 520
|
||||||
@@ -108,6 +110,32 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[], None]) -> None:
|
|||||||
RefreshField = _NumberField("Service page refresh (seconds)", Config.RefreshSeconds)
|
RefreshField = _NumberField("Service page refresh (seconds)", Config.RefreshSeconds)
|
||||||
LogLinesField = _NumberField("Log lines to show", Config.LogLines)
|
LogLinesField = _NumberField("Log lines to show", Config.LogLines)
|
||||||
|
|
||||||
|
SortdFileField = ft.TextField(value=str(Home / "SaraSortd-Configs.toml"), label="SaraSortd export file path")
|
||||||
|
|
||||||
|
def OnSortdExport(e) -> None:
|
||||||
|
PathText = (SortdFileField.value or "").strip()
|
||||||
|
if not PathText:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
Count = ExportSortd(Config, Path(PathText).expanduser())
|
||||||
|
except (OSError, SortdError) as Error:
|
||||||
|
TheApp.Notify(f"Couldn't export SaraSortd's configs: {Error}", "error")
|
||||||
|
return
|
||||||
|
TheApp.Notify(f"Exported {Count} SaraSortd configs.", "success")
|
||||||
|
|
||||||
|
def OnSortdImport(e) -> None:
|
||||||
|
PathText = (SortdFileField.value or "").strip()
|
||||||
|
if not PathText:
|
||||||
|
return
|
||||||
|
TheBundle, Error = ReadBundleSafely(Path(PathText).expanduser())
|
||||||
|
if TheBundle is None:
|
||||||
|
TheApp.Notify(Error, "error")
|
||||||
|
return
|
||||||
|
# Same as importing SaraSort's own settings: the Settings dialog
|
||||||
|
# closes, so a stale Save can't undo what the import wrote.
|
||||||
|
Page.pop_dialog()
|
||||||
|
ShowSortdImportDialog(TheApp, TheBundle, OnSaved)
|
||||||
|
|
||||||
def OnExport(e) -> None:
|
def OnExport(e) -> None:
|
||||||
PathText = (SettingsFileField.value or "").strip()
|
PathText = (SettingsFileField.value or "").strip()
|
||||||
if not PathText:
|
if not PathText:
|
||||||
@@ -177,6 +205,15 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[], None]) -> None:
|
|||||||
ft.Divider(height=1, color=Palette.Surface1),
|
ft.Divider(height=1, color=Palette.Surface1),
|
||||||
RefreshField,
|
RefreshField,
|
||||||
LogLinesField,
|
LogLinesField,
|
||||||
|
ft.Divider(height=1, color=Palette.Surface1),
|
||||||
|
_Note("Export/import all of SaraSortd's configs as one file: GlobalConf.toml, both templates "
|
||||||
|
"and every input/output directory's own config. Importing replaces the current ones "
|
||||||
|
"(they're backed up first)."),
|
||||||
|
SortdFileField,
|
||||||
|
ft.Row([
|
||||||
|
ft.OutlinedButton(content="Export", style=ChipButtonStyle(), on_click=OnSortdExport),
|
||||||
|
ft.OutlinedButton(content="Import", style=ChipButtonStyle(), on_click=OnSortdImport),
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
spacing=12,
|
spacing=12,
|
||||||
scroll=ft.ScrollMode.AUTO,
|
scroll=ft.ScrollMode.AUTO,
|
||||||
@@ -251,3 +288,73 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[], None]) -> None:
|
|||||||
ft.OutlinedButton(content="Save", style=ChipButtonStyle(), on_click=Save),
|
ft.OutlinedButton(content="Save", style=ChipButtonStyle(), on_click=Save),
|
||||||
],
|
],
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def ShowSortdImportDialog(TheApp: App, TheBundle: Bundle, OnDone: Callable[[], None]) -> None:
|
||||||
|
"""Confirms a SaraSortd import. Directories it uses that don't exist are
|
||||||
|
listed with a path field each: keep the path to have it made, or change it
|
||||||
|
to use another directory instead."""
|
||||||
|
Page = TheApp.Page
|
||||||
|
Config = TheApp.Config
|
||||||
|
try:
|
||||||
|
Missing = MissingDirectories(Config, TheBundle)
|
||||||
|
Total = len(BundleDirectories(Config, TheBundle))
|
||||||
|
except Exception as Error: # e.g. a GlobalConf in the export without the keys SaraSortd needs
|
||||||
|
TheApp.Notify(f"Can't use that export: {Error}", "error")
|
||||||
|
return
|
||||||
|
|
||||||
|
Fields: dict[str, ft.TextField] = {}
|
||||||
|
Rows: list[ft.Control] = []
|
||||||
|
for DirPath in Missing:
|
||||||
|
Row, Field = DirField(TheApp, f"{Path(DirPath).name or DirPath} (doesn't exist)", DirPath)
|
||||||
|
Fields[DirPath] = Field
|
||||||
|
Rows.append(Row)
|
||||||
|
|
||||||
|
Info = [
|
||||||
|
_Note(f"From an export of SaraSortd {TheBundle.SortdVersion or '?'}"
|
||||||
|
+ (f", made {TheBundle.Exported.replace('T', ' ')}" if TheBundle.Exported else "") + "."),
|
||||||
|
ft.Text(f"This replaces SaraSortd's configs in {Config.SortdConfigDir} and in {Total} "
|
||||||
|
f"director{'y' if Total == 1 else 'ies'}. The current ones are backed up to "
|
||||||
|
"~/.config/SaraSuite/SaraSort/Backups/ first.", size=13),
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
Daemon = Setup(Config).DaemonVersion()
|
||||||
|
except SortdError:
|
||||||
|
Daemon = None
|
||||||
|
if Daemon and TheBundle.SortdVersion and MajorVersion(Daemon) != MajorVersion(TheBundle.SortdVersion):
|
||||||
|
Info.append(ft.Text(f"These configs are from SaraSortd {TheBundle.SortdVersion}, but {Daemon} is installed: "
|
||||||
|
"the first number differs, so they may not work.", color=Palette.Red, size=13))
|
||||||
|
if Missing:
|
||||||
|
Info += [
|
||||||
|
ft.Divider(height=1, color=Palette.Surface1),
|
||||||
|
ft.Row([ft.Icon(ft.Icons.WARNING_AMBER, color=Palette.Yellow),
|
||||||
|
ft.Text(f"{len(Missing)} director{'y' if len(Missing) == 1 else 'ies'} "
|
||||||
|
f"{'doesn' if len(Missing) == 1 else 'don'}'t exist:", color=Palette.Yellow)]),
|
||||||
|
_Note("Keep a path to have it made, or change it to use another directory instead "
|
||||||
|
"(the configs and arrows follow it)."),
|
||||||
|
# Only the list scrolls, so the dialog stays as small as its content.
|
||||||
|
ft.Column(Rows, spacing=8, scroll=ft.ScrollMode.AUTO, height=min(300, 58 * len(Rows))),
|
||||||
|
]
|
||||||
|
|
||||||
|
def Continue(e) -> None:
|
||||||
|
Remap = {Old: ExpandPath(Field.value or "") or Old for Old, Field in Fields.items()}
|
||||||
|
Page.pop_dialog()
|
||||||
|
try:
|
||||||
|
ApplyBundle(Config, TheBundle, Remap)
|
||||||
|
except (OSError, SortdError) as Error:
|
||||||
|
TheApp.Notify(f"Import failed: {Error}", "error")
|
||||||
|
return
|
||||||
|
OnDone()
|
||||||
|
TheApp.AfterGlobalChange() # its toast says whether SaraSortd restarted
|
||||||
|
|
||||||
|
Page.show_dialog(ft.AlertDialog(
|
||||||
|
title="Import SaraSortd configs",
|
||||||
|
content=ft.Column(Info, width=620, tight=True, spacing=10),
|
||||||
|
actions=[
|
||||||
|
ft.OutlinedButton(content="Cancel", style=ChipButtonStyle(), on_click=lambda e: Page.pop_dialog()),
|
||||||
|
ft.OutlinedButton(
|
||||||
|
content="Make directories and import" if Missing else "Import",
|
||||||
|
style=ChipButtonStyle(Palette.Yellow if Missing else ""), on_click=Continue,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
))
|
||||||
@@ -114,16 +114,21 @@ class Setup:
|
|||||||
"""GlobalConf.toml plus everything reachable from it. Cheap to reload:
|
"""GlobalConf.toml plus everything reachable from it. Cheap to reload:
|
||||||
pages just make a new one after each change."""
|
pages just make a new one after each change."""
|
||||||
|
|
||||||
def __init__(self, Config: AppConfig):
|
def __init__(self, Config: AppConfig, Document: Optional[tomlkit.TOMLDocument] = None):
|
||||||
|
"""Document: a GlobalConf.toml that isn't on disk yet (an import being
|
||||||
|
checked); otherwise it's read from SortdConfigDir."""
|
||||||
self.Config = Config
|
self.Config = Config
|
||||||
self.ConfDir = Path(Config.SortdConfigDir).expanduser()
|
self.ConfDir = Path(Config.SortdConfigDir).expanduser()
|
||||||
self.GlobalPath = Config.GlobalConfPath
|
self.GlobalPath = Config.GlobalConfPath
|
||||||
if not self.GlobalPath.exists():
|
if Document is not None:
|
||||||
|
self.Global = Document
|
||||||
|
elif not self.GlobalPath.exists():
|
||||||
raise SortdError(f"No GlobalConf.toml at {self.GlobalPath}")
|
raise SortdError(f"No GlobalConf.toml at {self.GlobalPath}")
|
||||||
try:
|
else:
|
||||||
self.Global = ReadToml(self.GlobalPath)
|
try:
|
||||||
except tomlkit.exceptions.TOMLKitError as Error:
|
self.Global = ReadToml(self.GlobalPath)
|
||||||
raise SortdError(f"GlobalConf.toml can't be read: {Error}") from Error
|
except tomlkit.exceptions.TOMLKitError as Error:
|
||||||
|
raise SortdError(f"GlobalConf.toml can't be read: {Error}") from Error
|
||||||
self.Data = self.Global.unwrap()
|
self.Data = self.Global.unwrap()
|
||||||
self.Vars = self.Data.get("Variables", {})
|
self.Vars = self.Data.get("Variables", {})
|
||||||
self.Dirs = self.Data.get("DirectoryPaths", {})
|
self.Dirs = self.Data.get("DirectoryPaths", {})
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
"""Exporting and importing all of SaraSortd's configs as one file:
|
||||||
|
GlobalConf.toml, the two directory templates, and every input/output
|
||||||
|
directory's own config. Each is stored as its full text, so comments travel.
|
||||||
|
|
||||||
|
Format = "SaraSortdExport"
|
||||||
|
FormatVersion = 1
|
||||||
|
SaraSortdVersion = "7.1"
|
||||||
|
Exported = "2026-10-07T22:40:00"
|
||||||
|
|
||||||
|
[[Configs]]
|
||||||
|
Role = "Global" # Global, InputTemplate, OutputTemplate, Directory
|
||||||
|
Content = '''...'''
|
||||||
|
|
||||||
|
[[Configs]]
|
||||||
|
Role = "Directory"
|
||||||
|
Directory = "/home/sarah/Sorting/Pics" # resolved, as SaraSortd sees it
|
||||||
|
Kinds = ["In", "Out"] # which roles read this file
|
||||||
|
Content = '''...'''
|
||||||
|
|
||||||
|
On import, directories can be moved somewhere else: GlobalConf's InputDir and
|
||||||
|
OutputDir, every ValidInputDirs entry and the directory configs' own
|
||||||
|
ParentDir/Title follow the new path.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import tomlkit
|
||||||
|
|
||||||
|
from Core import AppConfig, ConfigDir
|
||||||
|
from Sortd import Setup, SortdError, TemplateNames, WriteToml
|
||||||
|
|
||||||
|
FormatName = "SaraSortdExport"
|
||||||
|
FormatVersion = 1
|
||||||
|
BackupsDir = ConfigDir / "Backups"
|
||||||
|
Roles = {"InputTemplate": "In", "OutputTemplate": "Out"}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DirectoryConf:
|
||||||
|
Directory: str
|
||||||
|
Kinds: list[str]
|
||||||
|
Content: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Bundle:
|
||||||
|
GlobalContent: str
|
||||||
|
Templates: dict[str, str] = field(default_factory=dict) # "In"/"Out" -> content
|
||||||
|
Directories: list[DirectoryConf] = field(default_factory=list)
|
||||||
|
SortdVersion: str = ""
|
||||||
|
Exported: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def ExportSortd(Config: AppConfig, DestPath: Path) -> int:
|
||||||
|
"""Writes the bundle; returns how many config files went in."""
|
||||||
|
TheSetup = Setup(Config)
|
||||||
|
Configs = tomlkit.aot()
|
||||||
|
|
||||||
|
def Add(Role: str, Content: str, **Extra) -> None:
|
||||||
|
Table = tomlkit.table()
|
||||||
|
Table["Role"] = Role
|
||||||
|
for Key, Value in Extra.items():
|
||||||
|
Table[Key] = Value
|
||||||
|
Table["Content"] = tomlkit.string(Content, multiline=True, literal="'''" not in Content)
|
||||||
|
Configs.append(Table)
|
||||||
|
|
||||||
|
Add("Global", TheSetup.GlobalPath.read_text(encoding="utf-8"))
|
||||||
|
for Kind, Role in (("In", "InputTemplate"), ("Out", "OutputTemplate")):
|
||||||
|
TemplatePath = TheSetup.TemplatePath(Kind)
|
||||||
|
if TemplatePath.exists():
|
||||||
|
Add(Role, TemplatePath.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
# One entry per file: a directory that is both input and output may use one file for both.
|
||||||
|
Files: dict[Path, tuple[str, list[str]]] = {}
|
||||||
|
for Kind, Paths in (("In", TheSetup.Inputs), ("Out", TheSetup.Outputs)):
|
||||||
|
for DirPath in dict.fromkeys(Paths):
|
||||||
|
ConfPath = TheSetup.ConfFile(DirPath, Kind)
|
||||||
|
if ConfPath.exists():
|
||||||
|
Files.setdefault(ConfPath, (DirPath, []))[1].append(Kind)
|
||||||
|
for ConfPath, (DirPath, Kinds) in Files.items():
|
||||||
|
Add("Directory", ConfPath.read_text(encoding="utf-8"), Directory=DirPath, Kinds=Kinds)
|
||||||
|
|
||||||
|
Document = tomlkit.document()
|
||||||
|
Document["Format"] = FormatName
|
||||||
|
Document["FormatVersion"] = FormatVersion
|
||||||
|
Document["SaraSortdVersion"] = TheSetup.DaemonVersion() or str(TheSetup.Data.get("Version", ""))
|
||||||
|
Document["Exported"] = datetime.datetime.now().isoformat(timespec="seconds")
|
||||||
|
Document["Configs"] = Configs
|
||||||
|
DestPath.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
DestPath.write_text(tomlkit.dumps(Document), encoding="utf-8")
|
||||||
|
return len(Configs)
|
||||||
|
|
||||||
|
|
||||||
|
def ReadBundle(SourcePath: Path) -> Bundle:
|
||||||
|
"""Raises SortdError when it isn't a usable SaraSortd export."""
|
||||||
|
try:
|
||||||
|
Data = tomlkit.parse(SourcePath.read_text(encoding="utf-8")).unwrap()
|
||||||
|
except (OSError, tomlkit.exceptions.TOMLKitError) as Error:
|
||||||
|
raise SortdError(f"Can't read {SourcePath}: {Error}") from Error
|
||||||
|
if Data.get("Format") != FormatName:
|
||||||
|
raise SortdError(f"{SourcePath.name} isn't a SaraSortd export (no Format = \"{FormatName}\").")
|
||||||
|
if int(Data.get("FormatVersion", 0)) > FormatVersion:
|
||||||
|
raise SortdError("That export was made by a newer SaraSort.")
|
||||||
|
Result = Bundle("", SortdVersion=str(Data.get("SaraSortdVersion", "")), Exported=str(Data.get("Exported", "")))
|
||||||
|
for Entry in Data.get("Configs", []):
|
||||||
|
Role, Content = Entry.get("Role"), str(Entry.get("Content", ""))
|
||||||
|
try:
|
||||||
|
tomlkit.parse(Content)
|
||||||
|
except tomlkit.exceptions.TOMLKitError as Error:
|
||||||
|
raise SortdError(f"A {Role} config in the export isn't valid TOML: {Error}") from Error
|
||||||
|
if Role == "Global":
|
||||||
|
Result.GlobalContent = Content
|
||||||
|
elif Role in Roles:
|
||||||
|
Result.Templates[Roles[Role]] = Content
|
||||||
|
elif Role == "Directory" and Entry.get("Directory"):
|
||||||
|
Result.Directories.append(DirectoryConf(str(Entry["Directory"]), list(Entry.get("Kinds", [])), Content))
|
||||||
|
if not Result.GlobalContent:
|
||||||
|
raise SortdError("The export has no GlobalConf.toml.")
|
||||||
|
return Result
|
||||||
|
|
||||||
|
|
||||||
|
def BundleSetup(Config: AppConfig, TheBundle: Bundle) -> Setup:
|
||||||
|
"""The imported GlobalConf, not written anywhere yet."""
|
||||||
|
return Setup(Config, tomlkit.parse(TheBundle.GlobalContent))
|
||||||
|
|
||||||
|
|
||||||
|
def BundleDirectories(Config: AppConfig, TheBundle: Bundle) -> list[str]:
|
||||||
|
"""Every input/output directory the import uses, in order."""
|
||||||
|
TheSetup = BundleSetup(Config, TheBundle)
|
||||||
|
Paths = TheSetup.Inputs + TheSetup.Outputs + [Entry.Directory for Entry in TheBundle.Directories]
|
||||||
|
return list(dict.fromkeys(Path for Path in Paths if Path))
|
||||||
|
|
||||||
|
|
||||||
|
def MissingDirectories(Config: AppConfig, TheBundle: Bundle) -> list[str]:
|
||||||
|
return [DirPath for DirPath in BundleDirectories(Config, TheBundle) if not os.path.isdir(DirPath)]
|
||||||
|
|
||||||
|
|
||||||
|
def Backup(Config: AppConfig) -> Optional[Path]:
|
||||||
|
"""Exports the current configs before an import replaces them."""
|
||||||
|
try:
|
||||||
|
Setup(Config)
|
||||||
|
except SortdError:
|
||||||
|
return None # nothing there to lose
|
||||||
|
Stamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
|
BackupPath = BackupsDir / f"SaraSortd-{Stamp}.toml"
|
||||||
|
ExportSortd(Config, BackupPath)
|
||||||
|
return BackupPath
|
||||||
|
|
||||||
|
|
||||||
|
def _RemapList(TheSetup: Setup, Values, Remap: dict[str, str], Parent: Optional[str] = None) -> None:
|
||||||
|
"""Points entries that resolve to a moved directory at its new path."""
|
||||||
|
if not isinstance(Values, list):
|
||||||
|
return
|
||||||
|
for Index in range(len(Values)):
|
||||||
|
Resolved = TheSetup.Parse(str(Values[Index]), Parent=Parent)
|
||||||
|
if Resolved in Remap:
|
||||||
|
Values[Index] = TheSetup.Shorthand(Remap[Resolved])
|
||||||
|
|
||||||
|
|
||||||
|
def ApplyBundle(Config: AppConfig, TheBundle: Bundle, Remap: dict[str, str]) -> Optional[Path]:
|
||||||
|
"""Writes the import. Remap moves directories: {old path: new path}.
|
||||||
|
Missing directories are made. Returns the backup's path (if any)."""
|
||||||
|
Remap = {Old: New for Old, New in Remap.items() if New and New != Old}
|
||||||
|
BackupPath = Backup(Config)
|
||||||
|
|
||||||
|
GlobalDoc = tomlkit.parse(TheBundle.GlobalContent)
|
||||||
|
TheSetup = Setup(Config, GlobalDoc)
|
||||||
|
Paths = GlobalDoc.get("DirectoryPaths", {})
|
||||||
|
for Key in ("InputDir", "OutputDir"):
|
||||||
|
_RemapList(TheSetup, Paths.get(Key), Remap)
|
||||||
|
|
||||||
|
ConfDir = Path(Config.SortdConfigDir).expanduser()
|
||||||
|
ConfDir.mkdir(parents=True, exist_ok=True)
|
||||||
|
WriteToml(Config.GlobalConfPath, GlobalDoc)
|
||||||
|
for Kind, Content in TheBundle.Templates.items():
|
||||||
|
(ConfDir / TemplateNames[Kind]).write_text(Content, encoding="utf-8")
|
||||||
|
|
||||||
|
TheSetup = Setup(Config) # the written GlobalConf, with the new paths
|
||||||
|
for Entry in TheBundle.Directories:
|
||||||
|
Target = Remap.get(Entry.Directory, Entry.Directory)
|
||||||
|
Document = tomlkit.parse(Entry.Content)
|
||||||
|
Parent = os.path.basename(Entry.Directory)
|
||||||
|
_RemapList(TheSetup, Document.get("ValidInputDirs"), Remap, Parent)
|
||||||
|
for FileEntry in Document.get("Files", []):
|
||||||
|
_RemapList(TheSetup, FileEntry.get("ValidInputDirs"), Remap, Parent)
|
||||||
|
NewName = os.path.basename(Target)
|
||||||
|
if NewName != Parent:
|
||||||
|
if "ParentDir" in Document:
|
||||||
|
Document["ParentDir"] = NewName
|
||||||
|
if "Title" in Document:
|
||||||
|
Document["Title"] = f"{NewName} Config"
|
||||||
|
Path(Target).mkdir(parents=True, exist_ok=True)
|
||||||
|
for Kind in dict.fromkeys(Entry.Kinds or ["Out"]):
|
||||||
|
WriteToml(TheSetup.ConfFile(Target, Kind), Document)
|
||||||
|
|
||||||
|
# Directories without a config in the export still need to exist.
|
||||||
|
for DirPath in TheSetup.Inputs + TheSetup.Outputs:
|
||||||
|
Path(DirPath).mkdir(parents=True, exist_ok=True)
|
||||||
|
return BackupPath
|
||||||
|
|
||||||
|
|
||||||
|
def ReadBundleSafely(SourcePath: Path) -> tuple[Optional[Bundle], str]:
|
||||||
|
try:
|
||||||
|
return ReadBundle(SourcePath), ""
|
||||||
|
except SortdError as Error:
|
||||||
|
return None, str(Error)
|
||||||
|
|
||||||
Reference in new issue
Block a user