Skip to content

Instantly share code, notes, and snippets.

@adde88
Created December 1, 2025 13:37
Show Gist options
  • Select an option

  • Save adde88/7293042b7fd3667ec7416f3d49a78d73 to your computer and use it in GitHub Desktop.

Select an option

Save adde88/7293042b7fd3667ec7416f3d49a78d73 to your computer and use it in GitHub Desktop.
Ollama-HardLinker: Interactive PowerShell script to import GGUF models using NTFS Hard Links (Zero-Duplication of Models to Save Storage) on Windows 11.
@echo off
:: Copyright (C) 2025 - Author: Andreas Nilsen
:: Launcher for Ollama Symlink Optimizer
:: Ensures script runs with Admin privileges
TITLE Ollama Model Manager & Symlinker
CLS
:checkPrivileges
NET FILE 1>NUL 2>NUL
if '%errorlevel%' == '0' ( goto gotPrivileges ) else ( goto getPrivileges )
:getPrivileges
if '%1'=='ELEV' (echo ELEV & shift /1 & goto gotPrivileges)
ECHO.
ECHO ***************************************
ECHO Requesting Administrator Privileges...
ECHO ***************************************
setlocal DisableDelayedExpansion
set "batchPath=%~0"
setlocal EnableDelayedExpansion
ECHO Set UAC = CreateObject^("Shell.Application"^) > "%temp%\OEgetPriv_ad.vbs"
ECHO UAC.ShellExecute "!batchPath!", "ELEV", "", "runas", 1 >> "%temp%\OEgetPriv_ad.vbs"
"%temp%\OEgetPriv_ad.vbs"
exit /B
:gotPrivileges
setlocal & pushd .
cd /d %~dp0
if exist "%temp%\OEgetPriv_ad.vbs" ( del "%temp%\OEgetPriv_ad.vbs" )
ECHO.
ECHO Running PowerShell Script...
PowerShell -NoProfile -ExecutionPolicy Bypass -File "Ollama-Symlinker.ps1"
ECHO.
ECHO Script finished. Press any key to exit.
PAUSE >NUL
<#
.SYNOPSIS
Interactive Ollama Model Creator & HardLink Optimizer
.DESCRIPTION
Scans for GGUF models and Modelfiles, creates Ollama models,
and replaces storage-heavy blobs with Hard Links to save space.
.NOTES
Copyright © 2025 - Author: Andreas Nilsen
Github: https://www.github.com/adde88 - [email protected]
Date: 01.12.25 (Updated for HardLink support)
#>
# --- Configuration ---
$BaseModelDir = "G:\LLM-MODELS\Models"
$BaseModelfileDir = "G:\LLM-MODELS\Modelfiles"
$OllamaBlobDir = "G:\LLM-MODELS\blobs"
$TempModelfile = "$env:TEMP\temp_create_ollama.Modelfile"
# --- Setup Console ---
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Clear-Host
Write-Host "==========================================================" -ForegroundColor Cyan
Write-Host " Ollama Model Manager & HardLink Optimizer - v1.1" -ForegroundColor White
Write-Host " Author: Andreas Nilsen (adde88)" -ForegroundColor DarkGray
Write-Host "==========================================================" -ForegroundColor Cyan
Write-Host ""
# --- Helper Functions ---
function Get-UserSelection {
param($Items, $PromptText)
Write-Host $PromptText -ForegroundColor Yellow
for ($i = 0; $i -lt $Items.Count; $i++) {
$filesize = ""
if ($Items[$i] -is [System.IO.FileInfo]) {
$gb = "{0:N2} GB" -f ($Items[$i].Length / 1GB)
$filesize = " [$gb]"
}
Write-Host " [$($i+1)] $($Items[$i].Name)$filesize" -ForegroundColor Green
}
while ($true) {
$selection = Read-Host "Select number (1-$($Items.Count))"
if ($selection -match "^\d+$" -and [int]$selection -ge 1 -and [int]$selection -le $Items.Count) {
return $Items[[int]$selection - 1]
}
Write-Host "Invalid selection. Try again." -ForegroundColor Red
}
}
function Sanitize-Input {
param($InputString)
return $InputString -replace '[^a-zA-Z0-9\.\-_:]', ''
}
# --- Step 1: Find GGUF Models ---
Write-Host "[1/5] Scanning for GGUF Models in: $BaseModelDir" -ForegroundColor Cyan
if (-not (Test-Path $BaseModelDir)) {
Write-Error "Model directory not found: $BaseModelDir"
pause; exit
}
$ggufFiles = Get-ChildItem -Path $BaseModelDir -Recurse -Filter "*.gguf" | Sort-Object Name
if ($ggufFiles.Count -eq 0) {
Write-Warning "No .gguf files found in $BaseModelDir"
pause; exit
}
$selectedGguf = Get-UserSelection -Items $ggufFiles -PromptText "Select the GGUF Model file you want to use:"
Write-Host "Selected Model: $($selectedGguf.FullName)" -ForegroundColor Magenta
Write-Host ""
# --- Step 2: Find Modelfile Templates ---
Write-Host "[2/5] Scanning for Modelfile templates in: $BaseModelfileDir" -ForegroundColor Cyan
if (-not (Test-Path $BaseModelfileDir)) {
Write-Error "Modelfile directory not found: $BaseModelfileDir"
pause; exit
}
$modelfiles = Get-ChildItem -Path $BaseModelfileDir -Filter "*Modelfile*" | Where-Object { $_.Extension -ne ".gguf" } | Sort-Object Name
if ($modelfiles.Count -eq 0) {
Write-Warning "No Modelfiles found in $BaseModelfileDir"
pause; exit
}
$selectedModelfile = Get-UserSelection -Items $modelfiles -PromptText "Select the Modelfile template to use:"
Write-Host "Selected Template: $($selectedModelfile.Name)" -ForegroundColor Magenta
Write-Host ""
# --- Step 3: Define Model Name ---
Write-Host "[3/5] Configuration" -ForegroundColor Cyan
$defaultName = $selectedModelfile.BaseName.ToLower().Replace(" ", "-")
$modelNameInput = Read-Host "Enter name for new Ollama model (Default: $defaultName)"
if ([string]::IsNullOrWhiteSpace($modelNameInput)) {
$modelName = $defaultName
} else {
$modelName = Sanitize-Input -InputString $modelNameInput
}
Write-Host "Target Model Name: $modelName" -ForegroundColor Magenta
Write-Host ""
# --- Step 4: Construct Temporary Modelfile ---
Write-Host "[4/5] Preparing Modelfile..." -ForegroundColor Cyan
# Read template content
$content = Get-Content -Path $selectedModelfile.FullName -Raw
# Convert Windows path to Forward Slashes for Ollama/Go compatibility
$ggufPath = $selectedGguf.FullName.Replace("\", "/")
# Logic: Check if FROM exists. If so, replace it. If not, prepend it.
if ($content -match "(?m)^FROM\s+.*") {
$newContent = $content -replace "(?m)^FROM\s+.*", "FROM $ggufPath"
} else {
$newContent = "FROM $ggufPath`n" + $content
}
# Save temp file
Set-Content -Path $TempModelfile -Value $newContent -Encoding UTF8
Write-Host "Temporary Modelfile generated with correct path." -ForegroundColor Green
Write-Host ""
# --- Step 5: Create & Optimize ---
Write-Host "[5/5] Running 'ollama create' and monitoring for Hash..." -ForegroundColor Cyan
Write-Host "Please wait. This may take a moment while Ollama processes the GGUF." -ForegroundColor Yellow
# Run Ollama and redirect output to capture the hash
$processInfo = New-Object System.Diagnostics.ProcessStartInfo
$processInfo.FileName = "ollama"
$processInfo.Arguments = "create $modelName -f ""$TempModelfile"""
$processInfo.RedirectStandardOutput = $true
$processInfo.RedirectStandardError = $true
$processInfo.UseShellExecute = $false
$processInfo.CreateNoWindow = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $processInfo
$outputData = new-object System.Text.StringBuilder
$errorData = new-object System.Text.StringBuilder
# Event handlers to capture output in real-time
$outputHandler = { param($sender, $e) if ($e.Data) { $null = $outputData.AppendLine($e.Data); Write-Host $e.Data } }
$errorHandler = { param($sender, $e) if ($e.Data) { $null = $errorData.AppendLine($e.Data); Write-Host $e.Data -ForegroundColor DarkGray } }
$process.add_OutputDataReceived($outputHandler)
$process.add_ErrorDataReceived($errorHandler)
$process.Start() | Out-Null
$process.BeginOutputReadLine()
$process.BeginErrorReadLine()
$process.WaitForExit()
# --- Analysis & HardLinking ---
$fullLog = $outputData.ToString() + $errorData.ToString()
# Look for the sha256 hash in the logs (Format: "copying file sha256:...")
if ($fullLog -match "sha256:([a-f0-9]{64})") {
$hash = $matches[1]
Write-Host "`nModel Hash Detected: $hash" -ForegroundColor Green
# Construct Blob Path
$blobName = "sha256-$hash"
$blobPath = Join-Path -Path $OllamaBlobDir -ChildPath $blobName
# Verify Blob Exists
if (Test-Path $blobPath) {
Write-Host "Found created blob at: $blobPath" -ForegroundColor Cyan
# Check drives (Hard Link requirement)
$sourceDrive = [System.IO.Path]::GetPathRoot($selectedGguf.FullName)
$destDrive = [System.IO.Path]::GetPathRoot($blobPath)
if ($sourceDrive -ne $destDrive) {
Write-Error "Cannot create Hard Link! Source ($sourceDrive) and Destination ($destDrive) are on different drives."
Write-Host "The model was created, but duplicate space is being used." -ForegroundColor Red
}
else {
# Check if it is already a Hard Link (File with >1 link count)
# PowerShell doesn't have a simple built-in property for LinkCount, so we try to create it.
# If we delete the blob and recreate as hardlink, we save space.
Write-Host "Optimizing storage... Deleting blob and creating Hard Link." -ForegroundColor Yellow
try {
Remove-Item -Path $blobPath -Force -ErrorAction Stop
# Create Hard Link
New-Item -ItemType HardLink -Path $blobPath -Target $selectedGguf.FullName -Force | Out-Null
Write-Host "SUCCESS: Blob replaced with Hard Link!" -ForegroundColor Green
Write-Host "Source: $($selectedGguf.FullName)" -ForegroundColor DarkGray
Write-Host "Target: $blobPath" -ForegroundColor DarkGray
Write-Host "Note: Hard Links look like normal files but share the same physical data." -ForegroundColor Gray
}
catch {
Write-Error "Failed to create Hard Link: $_"
Write-Host "Restoring blob..."
Copy-Item -Path $selectedGguf.FullName -Destination $blobPath
}
}
} else {
Write-Warning "Could not locate the blob file at $blobPath. Check your Ollama data directory configuration."
}
} else {
Write-Error "Could not parse SHA256 hash from Ollama output. Optimization skipped."
}
# Cleanup
if (Test-Path $TempModelfile) { Remove-Item $TempModelfile }
Write-Host "`nOperation Complete." -ForegroundColor Cyan
Write-Host "Copyright © 2025 - Author: Andreas Nilsen" -ForegroundColor DarkGray
Pause
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment