Compare commits

...

8 Commits

Author SHA1 Message Date
5b409d771b Merge branch 'main' into HOME 2025-11-10 18:52:32 -07:00
Nathan
169b4f656d fix updateseq batch run behavior 2025-11-10 15:47:05 -07:00
Nathan
a506c09a49 7z seq compression working 2025-11-10 11:35:25 -07:00
Nathan
83b5e62266 further deployment implementation to .config directory 2025-11-10 11:00:12 -07:00
Nathan
124ad9e3b6 support configloader deployment 2025-11-10 10:43:46 -07:00
Nathan
dfc113ea38 reconfig zipseq 2025-11-10 10:36:28 -07:00
Nathan
5c5e22cfb7 change config bools 2025-11-10 10:24:55 -07:00
Nathan
cef84c68b9 Merge remote-tracking branch 'HOME/HOME' 2025-11-10 10:19:06 -07:00
10 changed files with 19816 additions and 143 deletions

File diff suppressed because one or more lines are too long

View File

@@ -85,14 +85,84 @@ function Get-ProjectsRoot {
return $candidate
}
function Get-ProjectPathFromUser {
param(
[string]$Prompt = "Paste the project path to deploy in"
)
do {
$inputPath = Read-Host -Prompt $Prompt
if ([string]::IsNullOrWhiteSpace($inputPath)) {
Write-Warning "Path cannot be empty. Please try again."
continue
}
# Remove quotes if present
$inputPath = $inputPath.Trim('"', "'")
# Try to resolve the path
try {
if (Test-Path -LiteralPath $inputPath -PathType Container) {
$resolved = Resolve-Path -LiteralPath $inputPath -ErrorAction Stop
return $resolved.Path
}
else {
Write-Warning "Path does not exist or is not a directory: $inputPath"
$retry = Read-Host "Try again? (Y/N)"
if ($retry -notmatch '^[Yy]') {
return $null
}
}
}
catch {
Write-Warning "Invalid path: $($_.Exception.Message)"
$retry = Read-Host "Try again? (Y/N)"
if ($retry -notmatch '^[Yy]') {
return $null
}
}
} while ($true)
}
function Use-IsoDailyFormat {
$dailyFormat = Get-ConfigValue -Name 'dailyFormat' -Default $true
return [bool]$dailyFormat
# Handle backward compatibility with boolean values
if ($dailyFormat -is [bool]) {
return $dailyFormat
}
# Handle string values
if ($dailyFormat -is [string]) {
$formatStr = $dailyFormat.Trim()
# ISO format contains YYYY-MM-DD pattern
if ($formatStr -match 'YYYY-MM-DD' -or $formatStr -match '\d{4}-\d{2}-\d{2}') {
return $true
}
# daily_YYMMDD or other formats are not ISO
return $false
}
# Default to false (not ISO format) for unknown types
return $false
}
function Use-7Zip {
$zipper = Get-ConfigValue -Name 'zipper' -Default $true
return [bool]$zipper
# Handle backward compatibility with boolean values
if ($zipper -is [bool]) {
return $zipper
}
# Handle string values
if ($zipper -is [string]) {
$zipperStr = $zipper.Trim().ToLower()
return ($zipperStr -eq '7z')
}
# Default to false (use zip) for unknown types
return $false
}
function Get-ZipCompressionLevel {
@@ -111,3 +181,156 @@ function Get-ZipCompressionLevel {
return [Math]::Min(9, [Math]::Max(0, $value))
}
# If script is run directly (not dot-sourced), prompt for project path and deploy
# When dot-sourced, InvocationName is '.'; when run directly, it's the script path or name
if ($MyInvocation.InvocationName -ne '.') {
$projectPath = Get-ProjectPathFromUser
if ($null -ne $projectPath) {
# Deploy batch files and config to the project
$structDir = Get-StructDirectory
if (-not (Test-Path -LiteralPath $structDir -PathType Container)) {
Write-Error "Configured structDir not found: $structDir"
exit 1
}
try {
$resolvedProject = (Resolve-Path -LiteralPath $projectPath -ErrorAction Stop).Path
}
catch {
Write-Error "Unable to resolve project directory: $($_.Exception.Message)"
exit 1
}
if (-not (Test-Path -LiteralPath $resolvedProject -PathType Container)) {
Write-Error "Project path is not a directory: $resolvedProject"
exit 1
}
Write-Host "`nDeploying to: $resolvedProject" -ForegroundColor Cyan
Write-Host "Struct directory: $structDir" -ForegroundColor Cyan
$specs = @(
@{ Name = 'UpdateSequences.bat'; Source = Join-Path -Path $structDir -ChildPath 'UpdateSequences.bat' },
@{ Name = 'UpdateAllSequences.bat'; Source = Join-Path -Path $structDir -ChildPath 'UpdateAllSequences.bat' },
@{ Name = 'ZipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'ZipSeqArchv.bat' },
@{ Name = 'UnzipSeqArchv.bat'; Source = Join-Path -Path $structDir -ChildPath 'UnzipSeqArchv.bat' },
@{ Name = 'NewDaily.bat'; Source = Join-Path -Path $structDir -ChildPath 'NewDaily.bat' }
)
# Config files to deploy to projectroot\.config\
$configAssets = @(
@{ Name = 'config.json'; Source = Join-Path -Path $structDir -ChildPath 'config.json' },
@{ Name = 'GetStructDir.ps1'; Source = Join-Path -Path $structDir -ChildPath 'GetStructDir.ps1' }
)
foreach ($spec in $specs) {
if (-not (Test-Path -LiteralPath $spec.Source -PathType Leaf)) {
Write-Error "Source file not found: $($spec.Source)"
exit 1
}
}
foreach ($asset in $configAssets) {
if (-not (Test-Path -LiteralPath $asset.Source -PathType Leaf)) {
Write-Error "Config asset not found: $($asset.Source)"
exit 1
}
}
# Ensure .config directory exists in project root and is hidden
$projectConfigDir = Join-Path -Path $resolvedProject -ChildPath '.config'
if (-not (Test-Path -LiteralPath $projectConfigDir -PathType Container)) {
New-Item -Path $projectConfigDir -ItemType Directory -Force | Out-Null
Write-Host "Created .config directory: $projectConfigDir" -ForegroundColor Cyan
}
# Set hidden attribute on .config directory
$folder = Get-Item -LiteralPath $projectConfigDir -Force
$folder.Attributes = $folder.Attributes -bor [System.IO.FileAttributes]::Hidden
$touchedDirs = @{}
$summary = @()
foreach ($spec in $specs) {
Write-Host "`n=== Updating $($spec.Name) ===" -ForegroundColor Magenta
$targets = Get-ChildItem -LiteralPath $resolvedProject -Recurse -Filter $spec.Name -File -ErrorAction SilentlyContinue
$targets = $targets | Where-Object { $_.FullName -ne $spec.Source }
if (-not $targets) {
Write-Host "No targets found." -ForegroundColor Yellow
$summary += [pscustomobject]@{
Name = $spec.Name
Updated = 0
Failed = 0
Skipped = 0
Total = 0
}
continue
}
$updated = 0
$failed = 0
foreach ($target in $targets) {
try {
Copy-Item -Path $spec.Source -Destination $target.FullName -Force
Write-Host "[OK] $($target.FullName)" -ForegroundColor Green
$updated++
$targetDir = $target.Directory.FullName
if (-not $touchedDirs.ContainsKey($targetDir)) {
$touchedDirs[$targetDir] = $true
}
}
catch {
Write-Host "[FAIL] $($target.FullName)" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed
$failed++
}
}
$summary += [pscustomobject]@{
Name = $spec.Name
Updated = $updated
Failed = $failed
Skipped = 0
Total = @($targets).Count
}
}
Write-Host "`n=== Summary ===" -ForegroundColor Cyan
foreach ($item in $summary) {
Write-Host ("{0,-22} Updated: {1,3} Failed: {2,3} Total: {3,3}" -f $item.Name, $item.Updated, $item.Failed, $item.Total)
}
if (($summary | Measure-Object -Property Failed -Sum).Sum -gt 0) {
Write-Host "Completed with errors." -ForegroundColor Yellow
exit 1
}
Write-Host "All batch files refreshed successfully." -ForegroundColor Green
# Deploy config files to projectroot\.config\
Write-Host "`n=== Deploying config files to .config\ ===" -ForegroundColor Magenta
foreach ($asset in $configAssets) {
$targetPath = Join-Path -Path $projectConfigDir -ChildPath $asset.Name
try {
Copy-Item -Path $asset.Source -Destination $targetPath -Force
Write-Host "[OK] $targetPath" -ForegroundColor Green
}
catch {
Write-Host "[FAIL] $targetPath" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed
exit 1
}
}
Write-Host "`nConfig files deployed successfully." -ForegroundColor Green
Write-Host "Deployment completed successfully." -ForegroundColor Green
exit 0
}
else {
Write-Host "No project path provided." -ForegroundColor Yellow
exit 1
}
}

68
GetStructDir.ps1 Normal file
View File

@@ -0,0 +1,68 @@
# Simple helper script to get structDir from project config.json
# Reads config.json from .config folder in project root
param(
[string]$ProjectRoot
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrWhiteSpace($ProjectRoot)) {
# Try to determine project root from script location
if ($PSScriptRoot) {
$ProjectRoot = Split-Path -Parent $PSScriptRoot
}
elseif ($MyInvocation.MyCommand.Path) {
$ProjectRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
}
else {
Write-Error "Unable to determine project root. Please provide -ProjectRoot parameter."
exit 1
}
}
$configPath = Join-Path -Path $ProjectRoot -ChildPath '.config\config.json'
if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) {
Write-Error "config.json not found at: $configPath"
exit 1
}
try {
$config = Get-Content -LiteralPath $configPath -Raw -ErrorAction Stop | ConvertFrom-Json
if ($config.PSObject.Properties.Name -contains 'structDir') {
$structDir = $config.structDir
if ($null -ne $structDir -and ($structDir -isnot [string] -or $structDir.Trim().Length -gt 0)) {
# If it's an absolute path, resolve it
if ([System.IO.Path]::IsPathRooted($structDir)) {
$resolved = Resolve-Path -LiteralPath $structDir -ErrorAction SilentlyContinue
if ($null -ne $resolved) {
Write-Output $resolved.Path
exit 0
}
Write-Output $structDir
exit 0
}
# Relative path - resolve relative to config location
$candidate = Join-Path -Path (Split-Path -Parent $configPath) -ChildPath $structDir
$resolvedCandidate = Resolve-Path -LiteralPath $candidate -ErrorAction SilentlyContinue
if ($null -ne $resolvedCandidate) {
Write-Output $resolvedCandidate.Path
exit 0
}
Write-Output $candidate
exit 0
}
}
# Default: return the directory containing config.json (project root)
Write-Output $ProjectRoot
exit 0
}
catch {
Write-Error "Failed to read or parse config.json: $($_.Exception.Message)"
exit 1
}

View File

@@ -1,26 +1,27 @@
@echo off
setlocal EnableExtensions
setlocal EnableExtensions EnableDelayedExpansion
set "REN_DIR=%~dp0"
for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI"
set "CONFIG_LOADER=%REN_DIR%ConfigLoader.ps1"
set "CONFIG_PATH=%REN_DIR%config.json"
set "CONFIG_DIR=%PROJ_ROOT%\.config"
set "CONFIG_PATH=%CONFIG_DIR%\config.json"
set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1"
if not exist "%CONFIG_LOADER%" (
echo [ERROR] ConfigLoader.ps1 not found next to UnzipSeqArchv.bat.
echo Please run UpdateProjectBatches.ps1 to refresh helper files.
if not exist "%CONFIG_PATH%" (
echo [ERROR] config.json not found at %CONFIG_PATH%
echo Please run ConfigLoader.ps1 to deploy helper files.
exit /b 1
)
if not exist "%CONFIG_PATH%" (
echo [ERROR] config.json not found next to UnzipSeqArchv.bat.
echo Please run UpdateProjectBatches.ps1 to refresh helper files.
if not exist "%GET_STRUCT_DIR%" (
echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR%
echo Please run ConfigLoader.ps1 to deploy helper files.
exit /b 1
)
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"Set-StrictMode -Version Latest; $loader = Resolve-Path -LiteralPath '%CONFIG_LOADER%' -ErrorAction Stop; . $loader.Path; $pyPath = Join-Path (Get-StructDirectory) 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I"
"Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I"
if not defined PY_SCRIPT (
echo [ERROR] Unable to resolve zip_sequences.py path from config.
@@ -30,12 +31,12 @@ if not defined PY_SCRIPT (
pushd "%PROJ_ROOT%" >nul 2>&1
python "%PY_SCRIPT%" --mode expand --verbose %*
set "ERR=%ERRORLEVEL%"
set "ERR=!ERRORLEVEL!"
if not "%ERR%"=="0" (
echo Failed to expand render sequence archives (exit code %ERR%).
if not "!ERR!"=="0" (
echo Failed to expand render sequence archives (exit code !ERR!).
)
popd >nul 2>&1
exit /b %ERR%
exit /b !ERR!

View File

@@ -1,12 +1,12 @@
# Refresh helper batch scripts within a specific project directory.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
param(
[string]$ProjectPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if (-not $PSScriptRoot) {
$PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
}
@@ -55,9 +55,10 @@ $specs = @(
@{ Name = 'NewDaily.bat'; Source = Join-Path -Path $structDir -ChildPath 'NewDaily.bat' }
)
$sharedAssets = @(
@{ Name = 'ConfigLoader.ps1'; Source = Join-Path -Path $structDir -ChildPath 'ConfigLoader.ps1' },
@{ Name = 'config.json'; Source = Join-Path -Path $structDir -ChildPath 'config.json' }
# Config files to deploy to projectroot\.config\
$configAssets = @(
@{ Name = 'config.json'; Source = Join-Path -Path $structDir -ChildPath 'config.json' },
@{ Name = 'GetStructDir.ps1'; Source = Join-Path -Path $structDir -ChildPath 'GetStructDir.ps1' }
)
foreach ($spec in $specs) {
@@ -67,13 +68,20 @@ foreach ($spec in $specs) {
}
}
foreach ($asset in $sharedAssets) {
foreach ($asset in $configAssets) {
if (-not (Test-Path -LiteralPath $asset.Source -PathType Leaf)) {
Write-Error "Shared asset not found: $($asset.Source)"
Write-Error "Config asset not found: $($asset.Source)"
exit 1
}
}
# Ensure .config directory exists in project root
$projectConfigDir = Join-Path -Path $resolvedProject -ChildPath '.config'
if (-not (Test-Path -LiteralPath $projectConfigDir -PathType Container)) {
New-Item -Path $projectConfigDir -ItemType Directory -Force | Out-Null
Write-Host "Created .config directory: $projectConfigDir" -ForegroundColor Cyan
}
$touchedDirs = @{}
$summary = @()
@@ -101,24 +109,16 @@ foreach ($spec in $specs) {
foreach ($target in $targets) {
try {
Copy-Item -Path $spec.Source -Destination $target.FullName -Force
Write-Host " $($target.FullName)" -ForegroundColor Green
Write-Host "[OK] $($target.FullName)" -ForegroundColor Green
$updated++
$targetDir = $target.Directory.FullName
if (-not $touchedDirs.ContainsKey($targetDir)) {
foreach ($asset in $sharedAssets) {
try {
Copy-Item -Path $asset.Source -Destination (Join-Path -Path $targetDir -ChildPath $asset.Name) -Force
}
catch {
Write-Host " ✗ Failed to copy $($asset.Name) into $targetDir: $($_.Exception.Message)" -ForegroundColor Red
}
}
$touchedDirs[$targetDir] = $true
}
}
catch {
Write-Host " $($target.FullName)" -ForegroundColor Red
Write-Host "[FAIL] $($target.FullName)" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed
$failed++
}
@@ -129,7 +129,7 @@ foreach ($spec in $specs) {
Updated = $updated
Failed = $failed
Skipped = 0
Total = $targets.Count
Total = @($targets).Count
}
}
@@ -145,3 +145,20 @@ if (($summary | Measure-Object -Property Failed -Sum).Sum -gt 0) {
Write-Host "All batch files refreshed successfully." -ForegroundColor Green
# Deploy config files to projectroot\.config\
Write-Host "`n=== Deploying config files to .config\ ===" -ForegroundColor Magenta
foreach ($asset in $configAssets) {
$targetPath = Join-Path -Path $projectConfigDir -ChildPath $asset.Name
try {
Copy-Item -Path $asset.Source -Destination $targetPath -Force
Write-Host "[OK] $targetPath" -ForegroundColor Green
}
catch {
Write-Host "[FAIL] $targetPath" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed
exit 1
}
}
Write-Host "Config files deployed successfully." -ForegroundColor Green

View File

@@ -2,23 +2,26 @@
setlocal EnableExtensions
set "script_dir=%~dp0"
set "config_loader=%script_dir%ConfigLoader.ps1"
set "config_path=%script_dir%config.json"
for %%I in ("%script_dir%..\..") do set "PROJ_ROOT=%%~fI"
if not exist "%config_loader%" (
echo [ERROR] ConfigLoader.ps1 not found next to UpdateSequences.bat.
echo Please run UpdateProjectBatches.ps1 to refresh helper files.
set "CONFIG_DIR=%PROJ_ROOT%\.config"
set "CONFIG_PATH=%CONFIG_DIR%\config.json"
set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1"
if not exist "%CONFIG_PATH%" (
echo [ERROR] config.json not found at %CONFIG_PATH%
echo Please run ConfigLoader.ps1 to deploy helper files.
exit /b 1
)
if not exist "%config_path%" (
echo [ERROR] config.json not found next to UpdateSequences.bat.
echo Please run UpdateProjectBatches.ps1 to refresh helper files.
if not exist "%GET_STRUCT_DIR%" (
echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR%
echo Please run ConfigLoader.ps1 to deploy helper files.
exit /b 1
)
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"Set-StrictMode -Version Latest; $loader = Resolve-Path -LiteralPath '%config_loader%' -ErrorAction Stop; . $loader.Path; $ps1Path = Join-Path (Get-StructDirectory) 'UpdateSequences.ps1'; if (-not (Test-Path -LiteralPath $ps1Path)) { throw \"UpdateSequences.ps1 not found at $ps1Path\" }; Write-Output $ps1Path"`) do set "ps1=%%I"
"Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $ps1Path = Join-Path $structDir 'UpdateSequences.ps1'; if (-not (Test-Path -LiteralPath $ps1Path)) { throw \"UpdateSequences.ps1 not found at $ps1Path\" }; Write-Output $ps1Path"`) do set "ps1=%%I"
if not defined ps1 (
echo [ERROR] Unable to resolve UpdateSequences.ps1 path from config.

View File

@@ -1,26 +1,27 @@
@echo off
setlocal EnableExtensions
setlocal EnableExtensions EnableDelayedExpansion
set "REN_DIR=%~dp0"
for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI"
set "CONFIG_LOADER=%REN_DIR%ConfigLoader.ps1"
set "CONFIG_PATH=%REN_DIR%config.json"
set "CONFIG_DIR=%PROJ_ROOT%\.config"
set "CONFIG_PATH=%CONFIG_DIR%\config.json"
set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1"
if not exist "%CONFIG_LOADER%" (
echo [ERROR] ConfigLoader.ps1 not found next to ZipSeqArchv.bat.
echo Please run UpdateProjectBatches.ps1 to refresh helper files.
if not exist "%CONFIG_PATH%" (
echo [ERROR] config.json not found at %CONFIG_PATH%
echo Please run ConfigLoader.ps1 to deploy helper files.
exit /b 1
)
if not exist "%CONFIG_PATH%" (
echo [ERROR] config.json not found next to ZipSeqArchv.bat.
echo Please run UpdateProjectBatches.ps1 to refresh helper files.
if not exist "%GET_STRUCT_DIR%" (
echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR%
echo Please run ConfigLoader.ps1 to deploy helper files.
exit /b 1
)
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"Set-StrictMode -Version Latest; $loader = Resolve-Path -LiteralPath '%CONFIG_LOADER%' -ErrorAction Stop; . $loader.Path; $pyPath = Join-Path (Get-StructDirectory) 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I"
"Set-StrictMode -Version Latest; $structDir = & '%GET_STRUCT_DIR%' -ProjectRoot '%PROJ_ROOT%'; if (-not $structDir) { throw \"Failed to get structDir from GetStructDir.ps1\" }; $pyPath = Join-Path $structDir 'zip_sequences.py'; if (-not (Test-Path -LiteralPath $pyPath)) { throw \"zip_sequences.py not found at $pyPath\" }; Write-Output $pyPath"`) do set "PY_SCRIPT=%%I"
if not defined PY_SCRIPT (
echo [ERROR] Unable to resolve zip_sequences.py path from config.
@@ -30,12 +31,12 @@ if not defined PY_SCRIPT (
pushd "%PROJ_ROOT%" >nul 2>&1
python "%PY_SCRIPT%" --verbose %*
set "ERR=%ERRORLEVEL%"
set "ERR=!ERRORLEVEL!"
if not "%ERR%"=="0" (
echo Failed to update render sequence archives (exit code %ERR%).
if not "!ERR!"=="0" (
echo Failed to update render sequence archives (exit code !ERR!).
)
popd >nul 2>&1
exit /b %ERR%
exit /b !ERR!

View File

@@ -1,7 +1,7 @@
{
"dailyFormat": true,
"structDir": "D:\\ProjectStructure",
"zipper": true,
"dailyFormat": "daily_YYMMDD",
"structDir": "A:\\1 Amazon_Active_Projects\\3 ProjectStructure",
"zipper": "7z",
"compression": 9
}

View File

@@ -1,8 +0,0 @@
new script named UpgradeToGitProj. this is to be ran in a project structure that was pre-git.
1. appends gitignore and gitattributes, initializes git, and installs git lfs
- If already initialized, will this just error but continue?
2. Manages Renders folder:
a. creates folder, adding NewDaily and UpdateSeq scripts
b. scans the structure within blends\animations and creates a folder for each submodule, e.g. Horizontal, Shorts, Vertical, etc. If there are no submodules, it just grabs the daily_* folders.
c. For each daily_* folder, it copies the contents of each daily_*\seq\ folder into the Renders\submodule\ folder. If it's taking daily_* folders from the root, it just copies the contents of the daily_*\seq\ folder into the Renders\ folder.

View File

@@ -11,10 +11,12 @@ from __future__ import annotations
import argparse
import json
import subprocess
import os
import shutil
import subprocess
import sys
import tempfile
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Iterator, Sequence
@@ -31,36 +33,52 @@ SEQUENCE_EXTENSIONS = {
".exr",
}
STATE_SUFFIX = ".meta.json"
CONFIG_PATH = Path(__file__).resolve().with_name("config.json")
DEFAULT_CONFIG = {
"zipper": True,
"zipper": "7z",
"compression": 9,
"dailyFormat": "daily_YYMMDD",
}
def load_config() -> dict:
try:
text = CONFIG_PATH.read_text(encoding="utf-8")
except FileNotFoundError:
return DEFAULT_CONFIG.copy()
except OSError:
return DEFAULT_CONFIG.copy()
try:
data = json.loads(text)
except json.JSONDecodeError:
return DEFAULT_CONFIG.copy()
if not isinstance(data, dict):
return DEFAULT_CONFIG.copy()
merged = DEFAULT_CONFIG.copy()
merged.update(data)
return merged
# First try to load from project's .config folder (current working directory)
# Then fall back to ProjectStructure repo config (next to zip_sequences.py)
cwd = Path.cwd()
project_config = cwd / ".config" / "config.json"
repo_config = Path(__file__).resolve().with_name("config.json")
config_paths = [
("project", project_config),
("repo", repo_config),
]
for source, config_path in config_paths:
try:
if config_path.exists():
text = config_path.read_text(encoding="utf-8")
try:
data = json.loads(text)
if isinstance(data, dict):
merged = DEFAULT_CONFIG.copy()
merged.update(data)
return merged
except json.JSONDecodeError:
continue
except OSError:
continue
# If no config found, return defaults
return DEFAULT_CONFIG.copy()
CONFIG = load_config()
USE_7Z = bool(CONFIG.get("zipper", True))
zipper_val = CONFIG.get("zipper", "7z")
# Handle both old boolean format and new string format
if isinstance(zipper_val, bool):
ZIPPER_TYPE = "7z" if zipper_val else "zip"
else:
ZIPPER_TYPE = str(zipper_val).lower()
COMPRESSION_LEVEL = CONFIG.get("compression", 9)
if isinstance(COMPRESSION_LEVEL, str):
try:
@@ -72,11 +90,8 @@ if not isinstance(COMPRESSION_LEVEL, int):
COMPRESSION_LEVEL = max(0, min(9, COMPRESSION_LEVEL))
SEVEN_Z_EXE: str | None = None
if USE_7Z:
if ZIPPER_TYPE == "7z":
SEVEN_Z_EXE = shutil.which("7z") or shutil.which("7za")
if SEVEN_Z_EXE is None:
print("[zip] Requested 7z compression but no 7z executable was found; falling back to zipfile.", file=sys.stderr)
USE_7Z = False
def parse_args() -> argparse.Namespace:
@@ -172,7 +187,8 @@ def state_changed(seq_state: dict, stored_state: dict | None) -> bool:
def archive_path_for(seq_dir: Path) -> Path:
rel = seq_dir.relative_to(RENDER_ROOT)
return (ARCHIVE_ROOT / rel).with_suffix(".zip")
suffix = ".7z" if ZIPPER_TYPE == "7z" else ".zip"
return (ARCHIVE_ROOT / rel).with_suffix(suffix)
def sequence_dir_for(zip_path: Path) -> Path:
@@ -185,33 +201,130 @@ def state_path_for(zip_path: Path) -> Path:
def zip_sequence(seq_dir: Path, zip_path: Path) -> None:
if USE_7Z and SEVEN_Z_EXE:
if ZIPPER_TYPE == "7z":
if SEVEN_Z_EXE is None:
raise RuntimeError(
"7z compression requested but 7z executable not found in PATH. "
"Please install 7z (e.g., via Chocolatey: choco install 7zip) "
"or set zipper to 'zip' in config.json"
)
zip_path.parent.mkdir(parents=True, exist_ok=True)
cmd = [
SEVEN_Z_EXE,
"a",
"-y",
f"-mx={COMPRESSION_LEVEL}",
"-tzip",
str(zip_path),
".\\*",
]
subprocess.run(cmd, cwd=seq_dir, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# If creating a .7z file, remove any existing .zip file for the same sequence
if zip_path.suffix == ".7z":
old_zip_path = zip_path.with_suffix(".zip")
if old_zip_path.exists():
old_zip_path.unlink(missing_ok=True)
old_state_path = state_path_for(old_zip_path)
if old_state_path.exists():
old_state_path.unlink(missing_ok=True)
# Build list of files to archive with relative paths
file_list = []
for file_path in iter_sequence_files(seq_dir):
rel_path = file_path.relative_to(seq_dir).as_posix()
file_list.append(rel_path)
if not file_list:
raise RuntimeError(f"No files found to archive in {seq_dir}")
# Create zip in temporary location first to avoid issues with corrupted existing files
temp_zip = None
list_file_path = None
try:
# Create temporary archive file path (but don't create the file - let 7z create it)
temp_zip_path = tempfile.mktemp(suffix=".7z", dir=zip_path.parent)
temp_zip = Path(temp_zip_path)
# Create list file with absolute path
fd, temp_path = tempfile.mkstemp(suffix=".lst", text=True)
list_file_path = Path(temp_path)
with os.fdopen(fd, "w", encoding="utf-8") as list_file:
for rel_path in file_list:
list_file.write(rel_path + "\n")
list_file.flush()
os.fsync(list_file.fileno()) # Ensure data is written to disk
# File is closed here by context manager, small delay to ensure OS releases handle
time.sleep(0.1)
# Use absolute paths for both list file and temp zip
list_file_abs = list_file_path.resolve()
temp_zip_abs = temp_zip.resolve()
# Create archive in temp location first (7z will create it fresh)
cmd = [
SEVEN_Z_EXE,
"a",
"-y",
"-bb0", # Suppress progress output
f"-mx={COMPRESSION_LEVEL}",
"-t7z", # Use 7z format, not zip
str(temp_zip_abs),
f"@{list_file_abs}",
]
result = subprocess.run(
cmd,
cwd=seq_dir,
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if result.returncode != 0:
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
if result.stdout:
error_msg += f"\nstdout: {result.stdout.strip()}"
raise RuntimeError(f"7z compression failed: {error_msg}")
# Move temp zip to final location, replacing any existing file
if zip_path.exists():
zip_path.unlink()
temp_zip.replace(zip_path)
temp_zip = None # Mark as moved so we don't delete it
finally:
# Clean up temp zip if it wasn't moved
if temp_zip and temp_zip.exists():
try:
temp_zip.unlink(missing_ok=True)
except OSError:
pass
# Clean up list file, with retry in case 7z still has it open
if list_file_path and list_file_path.exists():
for attempt in range(3):
try:
list_file_path.unlink(missing_ok=True)
break
except PermissionError:
if attempt < 2:
time.sleep(0.1) # Wait 100ms before retry
else:
# Last attempt failed, just log and continue
# The temp file will be cleaned up by the OS eventually
pass
return
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
# Use zipfile (only if ZIPPER_TYPE == "zip")
if ZIPPER_TYPE == "zip":
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
zip_path.parent.mkdir(parents=True, exist_ok=True)
if COMPRESSION_LEVEL <= 0:
compression = ZIP_STORED
zip_kwargs = {}
else:
compression = ZIP_DEFLATED
zip_kwargs = {"compresslevel": COMPRESSION_LEVEL}
zip_path.parent.mkdir(parents=True, exist_ok=True)
if COMPRESSION_LEVEL <= 0:
compression = ZIP_STORED
zip_kwargs = {}
else:
compression = ZIP_DEFLATED
zip_kwargs = {"compresslevel": COMPRESSION_LEVEL}
with ZipFile(zip_path, "w", compression=compression, **zip_kwargs) as archive:
for file_path in iter_sequence_files(seq_dir):
archive.write(file_path, arcname=file_path.relative_to(seq_dir).as_posix())
with ZipFile(zip_path, "w", compression=compression, **zip_kwargs) as archive:
for file_path in iter_sequence_files(seq_dir):
archive.write(file_path, arcname=file_path.relative_to(seq_dir).as_posix())
return
# Unknown ZIPPER_TYPE - fail with clear error
raise RuntimeError(
f"Unsupported ZIPPER_TYPE: {ZIPPER_TYPE!r}. "
f"Expected '7z' or 'zip'. "
f"Config zipper value: {CONFIG.get('zipper', 'not set')!r}"
)
def expand_sequence(zip_path: Path, seq_state: dict) -> None:
@@ -220,7 +333,12 @@ def expand_sequence(zip_path: Path, seq_state: dict) -> None:
shutil.rmtree(target_dir)
target_dir.mkdir(parents=True, exist_ok=True)
if USE_7Z and SEVEN_Z_EXE:
if ZIPPER_TYPE == "7z":
if SEVEN_Z_EXE is None:
raise RuntimeError(
"7z extraction requested but 7z executable not found in PATH. "
"Please install 7z or set zipper to 'zip' in config.json"
)
cmd = [
SEVEN_Z_EXE,
"x",
@@ -228,12 +346,29 @@ def expand_sequence(zip_path: Path, seq_state: dict) -> None:
str(zip_path),
f"-o{target_dir}",
]
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
else:
result = subprocess.run(
cmd,
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if result.returncode != 0:
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
if result.stdout:
error_msg += f"\nstdout: {result.stdout.strip()}"
raise RuntimeError(f"7z extraction failed: {error_msg}")
elif ZIPPER_TYPE == "zip":
from zipfile import ZipFile
with ZipFile(zip_path, "r") as archive:
archive.extractall(target_dir)
else:
raise RuntimeError(
f"Unsupported ZIPPER_TYPE: {ZIPPER_TYPE!r}. "
f"Expected '7z' or 'zip'. "
f"Config zipper value: {CONFIG.get('zipper', 'not set')!r}"
)
for entry in seq_state.get("files", []):
file_path = target_dir / entry["path"]
@@ -262,11 +397,34 @@ def run_zip(worker_count: int, *, verbose: bool) -> int:
if not seq_state["files"]:
continue
# Get the target archive path (will be .7z if ZIPPER_TYPE is "7z")
zip_path = archive_path_for(seq_dir)
state_path = state_path_for(zip_path)
# Check if we need to upgrade from .zip to .7z
old_zip_path = None
if ZIPPER_TYPE == "7z":
# Check if an old .zip file exists
old_zip_path = zip_path.with_suffix(".zip")
if old_zip_path.exists():
# Check if the old .zip's metadata matches current state
old_state_path = state_path_for(old_zip_path)
old_stored_state = load_state(old_state_path)
if not state_changed(seq_state, old_stored_state):
# Old .zip is up to date, skip conversion
continue
# Old .zip is out of date, will be replaced with .7z
# Check if the target archive (e.g., .7z) already exists and is up to date
stored_state = load_state(state_path)
if not state_changed(seq_state, stored_state):
# Target archive is up to date, but we might still need to clean up old .zip
if old_zip_path and old_zip_path.exists():
# Old .zip exists but we have a newer .7z, remove the old one
old_zip_path.unlink(missing_ok=True)
old_state_path = state_path_for(old_zip_path)
if old_state_path.exists():
old_state_path.unlink(missing_ok=True)
continue
work_items.append((seq_dir, zip_path, state_path, seq_state))
@@ -320,18 +478,21 @@ def run_expand(worker_count: int, *, verbose: bool) -> int:
work_items: list[tuple[Path, dict]] = []
for zip_path in ARCHIVE_ROOT.rglob("*.zip"):
state_path = state_path_for(zip_path)
seq_state = load_state(state_path)
if seq_state is None:
log("expand", f"Skipping {zip_path} (missing metadata)")
continue
# Look for both .zip and .7z archives
archive_patterns = ["*.zip", "*.7z"]
for pattern in archive_patterns:
for zip_path in ARCHIVE_ROOT.rglob(pattern):
state_path = state_path_for(zip_path)
seq_state = load_state(state_path)
if seq_state is None:
log("expand", f"Skipping {zip_path} (missing metadata)")
continue
target_dir = sequence_dir_for(zip_path)
if current_state(target_dir) == seq_state:
continue
target_dir = sequence_dir_for(zip_path)
if current_state(target_dir) == seq_state:
continue
work_items.append((zip_path, seq_state))
work_items.append((zip_path, seq_state))
if not work_items:
log("expand", "Working folders already match archives; nothing to expand.")
@@ -363,19 +524,22 @@ def cleanup_orphan_archives(*, verbose: bool) -> int:
removed: list[Path] = []
for zip_path in ARCHIVE_ROOT.rglob("*.zip"):
seq_dir = sequence_dir_for(zip_path)
if seq_dir.exists():
continue
# Look for both .zip and .7z archives
archive_patterns = ["*.zip", "*.7z"]
for pattern in archive_patterns:
for zip_path in ARCHIVE_ROOT.rglob(pattern):
seq_dir = sequence_dir_for(zip_path)
if seq_dir.exists():
continue
rel = zip_path.relative_to(ARCHIVE_ROOT)
log("zip", f"Removing orphan archive {rel}", verbose_only=True, verbose=verbose)
rel = zip_path.relative_to(ARCHIVE_ROOT)
log("zip", f"Removing orphan archive {rel}", verbose_only=True, verbose=verbose)
zip_path.unlink(missing_ok=True)
state_path = state_path_for(zip_path)
if state_path.exists():
state_path.unlink()
removed.append(zip_path)
zip_path.unlink(missing_ok=True)
state_path = state_path_for(zip_path)
if state_path.exists():
state_path.unlink()
removed.append(zip_path)
if not removed:
return 0