3 min read

Handling Zip Path Separators for Linux/cPanel Compatibility

The Problem

When creating a zip file on a Windows machine for use on a Linux-based server (like a typical cPanel/FTP environment), a common issue arises with directory path separators.

  • Windows uses backslashes (\): assets\js\script.js
  • Linux/Zip Standard uses forward slashes (/): assets/js/script.js

PowerShell's default Compress-Archive cmdlet, and even some underlying .NET methods, may create zip archives using Windows-style backslashes. When a Linux server tries to unzip this archive, it doesn't recognize the backslash as a directory separator. Instead of creating a js folder inside an assets folder, it might create a single file literally named assets\js\script.js, which breaks the application (e.g., a WordPress theme).

The Solution: Manual Zip Creation

To guarantee that the zip file uses the correct forward-slash separators, we must bypass the high-level cmdlets and manually construct the archive. This gives us explicit control over the name of each entry within the zip file.

The following PowerShell code, implemented in scripts/bundle.ps1, demonstrates the correct approach.

# Manually create ZipArchive to enforce forward slashes for Linux/FTP compatibility
Add-Type -AssemblyName System.IO.Compression.FileSystem
$ZipArchive = [System.IO.Compression.ZipFile]::Open($ZipPath, [System.IO.Compression.ZipArchiveMode]::Create)
$ThemeBuildDirFull = (Get-Item $ThemeBuildDir).FullName
$Files = Get-ChildItem -Path $ThemeBuildDir -Recurse -File
foreach ($File in $Files) {
    # Get the path of the file relative to the source directory
    $RelativePath = $File.FullName.Substring($ThemeBuildDirFull.Length + 1)
    # CRITICAL: Replace Windows backslashes with Linux/Zip-standard forward slashes
    $EntryName = $RelativePath -replace '\\', '/'
    # Add the file to the archive with the corrected entry name
    [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($ZipArchive, $File.FullName, $EntryName)
}
# Finalize and close the zip file
$ZipArchive.Dispose()

How It Works

  1. Add-Type: Loads the necessary .NET assembly for advanced file compression operations.
  2. [System.IO.Compression.ZipFile]::Open(...): Creates a new, empty zip archive stream that we can write to.
  3. Get-ChildItem -Recurse -File: Gathers a list of all files (not directories) within our source build directory.
  4. foreach ($File in $Files): We loop through each file we need to add.
  5. $File.FullName.Substring(...): This calculates the file's path relative to the build directory (e.g., assets\js\script.js).
  6. $RelativePath -replace '\\', '/': This is the most important step. It takes the relative path and explicitly replaces every backslash (\) with a forward slash (/).
  7. CreateEntryFromFile(...): This adds the current file to the zip archive, but it uses our corrected $EntryName (with forward slashes) as the internal path.
  8. $ZipArchive.Dispose(): This closes the stream and saves the final, correctly formatted zip file to disk.

By following this method, the bundle.ps1 script produces a universally compatible zip file that will be correctly extracted on any server environment.