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 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 { function Use-IsoDailyFormat {
$dailyFormat = Get-ConfigValue -Name 'dailyFormat' -Default $true $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 { function Use-7Zip {
$zipper = Get-ConfigValue -Name 'zipper' -Default $true $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 { function Get-ZipCompressionLevel {
@@ -111,3 +181,156 @@ function Get-ZipCompressionLevel {
return [Math]::Min(9, [Math]::Max(0, $value)) 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 @echo off
setlocal EnableExtensions setlocal EnableExtensions EnableDelayedExpansion
set "REN_DIR=%~dp0" set "REN_DIR=%~dp0"
for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI" for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI"
set "CONFIG_LOADER=%REN_DIR%ConfigLoader.ps1" set "CONFIG_DIR=%PROJ_ROOT%\.config"
set "CONFIG_PATH=%REN_DIR%config.json" set "CONFIG_PATH=%CONFIG_DIR%\config.json"
set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1"
if not exist "%CONFIG_LOADER%" ( if not exist "%CONFIG_PATH%" (
echo [ERROR] ConfigLoader.ps1 not found next to UnzipSeqArchv.bat. echo [ERROR] config.json not found at %CONFIG_PATH%
echo Please run UpdateProjectBatches.ps1 to refresh helper files. echo Please run ConfigLoader.ps1 to deploy helper files.
exit /b 1 exit /b 1
) )
if not exist "%CONFIG_PATH%" ( if not exist "%GET_STRUCT_DIR%" (
echo [ERROR] config.json not found next to UnzipSeqArchv.bat. echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR%
echo Please run UpdateProjectBatches.ps1 to refresh helper files. echo Please run ConfigLoader.ps1 to deploy helper files.
exit /b 1 exit /b 1
) )
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^ 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 ( if not defined PY_SCRIPT (
echo [ERROR] Unable to resolve zip_sequences.py path from config. 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 pushd "%PROJ_ROOT%" >nul 2>&1
python "%PY_SCRIPT%" --mode expand --verbose %* python "%PY_SCRIPT%" --mode expand --verbose %*
set "ERR=%ERRORLEVEL%" set "ERR=!ERRORLEVEL!"
if not "%ERR%"=="0" ( if not "!ERR!"=="0" (
echo Failed to expand render sequence archives (exit code %ERR%). echo Failed to expand render sequence archives (exit code !ERR!).
) )
popd >nul 2>&1 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. # Refresh helper batch scripts within a specific project directory.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
param( param(
[string]$ProjectPath [string]$ProjectPath
) )
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if (-not $PSScriptRoot) { if (-not $PSScriptRoot) {
$PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path $PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
} }
@@ -55,9 +55,10 @@ $specs = @(
@{ Name = 'NewDaily.bat'; Source = Join-Path -Path $structDir -ChildPath 'NewDaily.bat' } @{ Name = 'NewDaily.bat'; Source = Join-Path -Path $structDir -ChildPath 'NewDaily.bat' }
) )
$sharedAssets = @( # Config files to deploy to projectroot\.config\
@{ Name = 'ConfigLoader.ps1'; Source = Join-Path -Path $structDir -ChildPath 'ConfigLoader.ps1' }, $configAssets = @(
@{ Name = 'config.json'; Source = Join-Path -Path $structDir -ChildPath 'config.json' } @{ 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) { 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)) { 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 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 = @{} $touchedDirs = @{}
$summary = @() $summary = @()
@@ -101,24 +109,16 @@ foreach ($spec in $specs) {
foreach ($target in $targets) { foreach ($target in $targets) {
try { try {
Copy-Item -Path $spec.Source -Destination $target.FullName -Force Copy-Item -Path $spec.Source -Destination $target.FullName -Force
Write-Host " $($target.FullName)" -ForegroundColor Green Write-Host "[OK] $($target.FullName)" -ForegroundColor Green
$updated++ $updated++
$targetDir = $target.Directory.FullName $targetDir = $target.Directory.FullName
if (-not $touchedDirs.ContainsKey($targetDir)) { 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 $touchedDirs[$targetDir] = $true
} }
} }
catch { catch {
Write-Host " $($target.FullName)" -ForegroundColor Red Write-Host "[FAIL] $($target.FullName)" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed Write-Host " $($_.Exception.Message)" -ForegroundColor DarkRed
$failed++ $failed++
} }
@@ -129,7 +129,7 @@ foreach ($spec in $specs) {
Updated = $updated Updated = $updated
Failed = $failed Failed = $failed
Skipped = 0 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 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 setlocal EnableExtensions
set "script_dir=%~dp0" set "script_dir=%~dp0"
set "config_loader=%script_dir%ConfigLoader.ps1" for %%I in ("%script_dir%..\..") do set "PROJ_ROOT=%%~fI"
set "config_path=%script_dir%config.json"
if not exist "%config_loader%" ( set "CONFIG_DIR=%PROJ_ROOT%\.config"
echo [ERROR] ConfigLoader.ps1 not found next to UpdateSequences.bat. set "CONFIG_PATH=%CONFIG_DIR%\config.json"
echo Please run UpdateProjectBatches.ps1 to refresh helper files. 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 exit /b 1
) )
if not exist "%config_path%" ( if not exist "%GET_STRUCT_DIR%" (
echo [ERROR] config.json not found next to UpdateSequences.bat. echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR%
echo Please run UpdateProjectBatches.ps1 to refresh helper files. echo Please run ConfigLoader.ps1 to deploy helper files.
exit /b 1 exit /b 1
) )
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^ 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 ( if not defined ps1 (
echo [ERROR] Unable to resolve UpdateSequences.ps1 path from config. echo [ERROR] Unable to resolve UpdateSequences.ps1 path from config.

View File

@@ -1,26 +1,27 @@
@echo off @echo off
setlocal EnableExtensions setlocal EnableExtensions EnableDelayedExpansion
set "REN_DIR=%~dp0" set "REN_DIR=%~dp0"
for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI" for %%I in ("%REN_DIR%..") do set "PROJ_ROOT=%%~fI"
set "CONFIG_LOADER=%REN_DIR%ConfigLoader.ps1" set "CONFIG_DIR=%PROJ_ROOT%\.config"
set "CONFIG_PATH=%REN_DIR%config.json" set "CONFIG_PATH=%CONFIG_DIR%\config.json"
set "GET_STRUCT_DIR=%CONFIG_DIR%\GetStructDir.ps1"
if not exist "%CONFIG_LOADER%" ( if not exist "%CONFIG_PATH%" (
echo [ERROR] ConfigLoader.ps1 not found next to ZipSeqArchv.bat. echo [ERROR] config.json not found at %CONFIG_PATH%
echo Please run UpdateProjectBatches.ps1 to refresh helper files. echo Please run ConfigLoader.ps1 to deploy helper files.
exit /b 1 exit /b 1
) )
if not exist "%CONFIG_PATH%" ( if not exist "%GET_STRUCT_DIR%" (
echo [ERROR] config.json not found next to ZipSeqArchv.bat. echo [ERROR] GetStructDir.ps1 not found at %GET_STRUCT_DIR%
echo Please run UpdateProjectBatches.ps1 to refresh helper files. echo Please run ConfigLoader.ps1 to deploy helper files.
exit /b 1 exit /b 1
) )
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command ^ 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 ( if not defined PY_SCRIPT (
echo [ERROR] Unable to resolve zip_sequences.py path from config. 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 pushd "%PROJ_ROOT%" >nul 2>&1
python "%PY_SCRIPT%" --verbose %* python "%PY_SCRIPT%" --verbose %*
set "ERR=%ERRORLEVEL%" set "ERR=!ERRORLEVEL!"
if not "%ERR%"=="0" ( if not "!ERR!"=="0" (
echo Failed to update render sequence archives (exit code %ERR%). echo Failed to update render sequence archives (exit code !ERR!).
) )
popd >nul 2>&1 popd >nul 2>&1
exit /b %ERR% exit /b !ERR!

View File

@@ -1,7 +1,7 @@
{ {
"dailyFormat": true, "dailyFormat": "daily_YYMMDD",
"structDir": "D:\\ProjectStructure", "structDir": "A:\\1 Amazon_Active_Projects\\3 ProjectStructure",
"zipper": true, "zipper": "7z",
"compression": 9 "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 argparse
import json import json
import subprocess
import os import os
import shutil import shutil
import subprocess
import sys import sys
import tempfile
import time
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
from typing import Iterator, Sequence from typing import Iterator, Sequence
@@ -31,36 +33,52 @@ SEQUENCE_EXTENSIONS = {
".exr", ".exr",
} }
STATE_SUFFIX = ".meta.json" STATE_SUFFIX = ".meta.json"
CONFIG_PATH = Path(__file__).resolve().with_name("config.json")
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
"zipper": True, "zipper": "7z",
"compression": 9, "compression": 9,
"dailyFormat": "daily_YYMMDD",
} }
def load_config() -> dict: def load_config() -> dict:
try: # First try to load from project's .config folder (current working directory)
text = CONFIG_PATH.read_text(encoding="utf-8") # Then fall back to ProjectStructure repo config (next to zip_sequences.py)
except FileNotFoundError: cwd = Path.cwd()
return DEFAULT_CONFIG.copy() project_config = cwd / ".config" / "config.json"
except OSError: repo_config = Path(__file__).resolve().with_name("config.json")
return DEFAULT_CONFIG.copy()
config_paths = [
try: ("project", project_config),
data = json.loads(text) ("repo", repo_config),
except json.JSONDecodeError: ]
return DEFAULT_CONFIG.copy()
for source, config_path in config_paths:
if not isinstance(data, dict): try:
return DEFAULT_CONFIG.copy() if config_path.exists():
text = config_path.read_text(encoding="utf-8")
merged = DEFAULT_CONFIG.copy() try:
merged.update(data) data = json.loads(text)
return merged 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() 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) COMPRESSION_LEVEL = CONFIG.get("compression", 9)
if isinstance(COMPRESSION_LEVEL, str): if isinstance(COMPRESSION_LEVEL, str):
try: try:
@@ -72,11 +90,8 @@ if not isinstance(COMPRESSION_LEVEL, int):
COMPRESSION_LEVEL = max(0, min(9, COMPRESSION_LEVEL)) COMPRESSION_LEVEL = max(0, min(9, COMPRESSION_LEVEL))
SEVEN_Z_EXE: str | None = None SEVEN_Z_EXE: str | None = None
if USE_7Z: if ZIPPER_TYPE == "7z":
SEVEN_Z_EXE = shutil.which("7z") or shutil.which("7za") 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: 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: def archive_path_for(seq_dir: Path) -> Path:
rel = seq_dir.relative_to(RENDER_ROOT) 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: 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: 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) zip_path.parent.mkdir(parents=True, exist_ok=True)
cmd = [
SEVEN_Z_EXE, # If creating a .7z file, remove any existing .zip file for the same sequence
"a", if zip_path.suffix == ".7z":
"-y", old_zip_path = zip_path.with_suffix(".zip")
f"-mx={COMPRESSION_LEVEL}", if old_zip_path.exists():
"-tzip", old_zip_path.unlink(missing_ok=True)
str(zip_path), old_state_path = state_path_for(old_zip_path)
".\\*", if old_state_path.exists():
] old_state_path.unlink(missing_ok=True)
subprocess.run(cmd, cwd=seq_dir, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# 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 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) zip_path.parent.mkdir(parents=True, exist_ok=True)
if COMPRESSION_LEVEL <= 0: if COMPRESSION_LEVEL <= 0:
compression = ZIP_STORED compression = ZIP_STORED
zip_kwargs = {} zip_kwargs = {}
else: else:
compression = ZIP_DEFLATED compression = ZIP_DEFLATED
zip_kwargs = {"compresslevel": COMPRESSION_LEVEL} zip_kwargs = {"compresslevel": COMPRESSION_LEVEL}
with ZipFile(zip_path, "w", compression=compression, **zip_kwargs) as archive: with ZipFile(zip_path, "w", compression=compression, **zip_kwargs) as archive:
for file_path in iter_sequence_files(seq_dir): for file_path in iter_sequence_files(seq_dir):
archive.write(file_path, arcname=file_path.relative_to(seq_dir).as_posix()) 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: 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) shutil.rmtree(target_dir)
target_dir.mkdir(parents=True, exist_ok=True) 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 = [ cmd = [
SEVEN_Z_EXE, SEVEN_Z_EXE,
"x", "x",
@@ -228,12 +346,29 @@ def expand_sequence(zip_path: Path, seq_state: dict) -> None:
str(zip_path), str(zip_path),
f"-o{target_dir}", f"-o{target_dir}",
] ]
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result = subprocess.run(
else: 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 from zipfile import ZipFile
with ZipFile(zip_path, "r") as archive: with ZipFile(zip_path, "r") as archive:
archive.extractall(target_dir) 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", []): for entry in seq_state.get("files", []):
file_path = target_dir / entry["path"] file_path = target_dir / entry["path"]
@@ -262,11 +397,34 @@ def run_zip(worker_count: int, *, verbose: bool) -> int:
if not seq_state["files"]: if not seq_state["files"]:
continue continue
# Get the target archive path (will be .7z if ZIPPER_TYPE is "7z")
zip_path = archive_path_for(seq_dir) zip_path = archive_path_for(seq_dir)
state_path = state_path_for(zip_path) 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) stored_state = load_state(state_path)
if not state_changed(seq_state, stored_state): 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 continue
work_items.append((seq_dir, zip_path, state_path, seq_state)) 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]] = [] work_items: list[tuple[Path, dict]] = []
for zip_path in ARCHIVE_ROOT.rglob("*.zip"): # Look for both .zip and .7z archives
state_path = state_path_for(zip_path) archive_patterns = ["*.zip", "*.7z"]
seq_state = load_state(state_path) for pattern in archive_patterns:
if seq_state is None: for zip_path in ARCHIVE_ROOT.rglob(pattern):
log("expand", f"Skipping {zip_path} (missing metadata)") state_path = state_path_for(zip_path)
continue 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) target_dir = sequence_dir_for(zip_path)
if current_state(target_dir) == seq_state: if current_state(target_dir) == seq_state:
continue continue
work_items.append((zip_path, seq_state)) work_items.append((zip_path, seq_state))
if not work_items: if not work_items:
log("expand", "Working folders already match archives; nothing to expand.") log("expand", "Working folders already match archives; nothing to expand.")
@@ -363,19 +524,22 @@ def cleanup_orphan_archives(*, verbose: bool) -> int:
removed: list[Path] = [] removed: list[Path] = []
for zip_path in ARCHIVE_ROOT.rglob("*.zip"): # Look for both .zip and .7z archives
seq_dir = sequence_dir_for(zip_path) archive_patterns = ["*.zip", "*.7z"]
if seq_dir.exists(): for pattern in archive_patterns:
continue 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) rel = zip_path.relative_to(ARCHIVE_ROOT)
log("zip", f"Removing orphan archive {rel}", verbose_only=True, verbose=verbose) log("zip", f"Removing orphan archive {rel}", verbose_only=True, verbose=verbose)
zip_path.unlink(missing_ok=True) zip_path.unlink(missing_ok=True)
state_path = state_path_for(zip_path) state_path = state_path_for(zip_path)
if state_path.exists(): if state_path.exists():
state_path.unlink() state_path.unlink()
removed.append(zip_path) removed.append(zip_path)
if not removed: if not removed:
return 0 return 0