mirror of
https://github.com/iminet/dev.git
synced 2026-11-04 10:09:15 +01:00
Added ImgBB_Uploader PowerShell script
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Upload JPG, JPEG, and PNG files to ImgBB sequentially.
|
||||
.DESCRIPTION
|
||||
Uses the official HTTPS API without external module dependencies. Files are
|
||||
streamed through the pipeline without a configured file-count limit.
|
||||
Requests are spaced 3-6 seconds apart by default. Transient failures get at most three
|
||||
retries with exponential backoff and jitter; Retry-After is respected.
|
||||
Exhausted transient failures stop the run to avoid sustained server load.
|
||||
Automation is not disguised and service-side limits still apply.
|
||||
Console colors: green for success, cyan for status/dry run, yellow for
|
||||
retries/skips, and red for errors. Text labels remain readable without color.
|
||||
.PARAMETER DirectoryPath
|
||||
An existing directory. Treated as a literal path, including brackets.
|
||||
.PARAMETER ApiKey
|
||||
ImgBB API key. A nonblank value overrides IMGBB_API_KEY. Prefer the environment
|
||||
variable to avoid saving a key in shell history. The key is sent in the POST
|
||||
body, never in the URL or script-generated messages.
|
||||
.PARAMETER Recurse
|
||||
Include subdirectories. Disabled by default. Hidden files are not included.
|
||||
.PARAMETER WaitTime
|
||||
Random (default) enables a random 3-6 second pause between requests.
|
||||
Only Off disables this ordinary pause. Omitted, empty, or unrecognized
|
||||
values retain random waiting. Values are case-insensitive. Retry backoff
|
||||
and server Retry-After waits remain enabled even with Off.
|
||||
.PARAMETER Silent
|
||||
Show errors only; suppress file status, retry notices, and summaries.
|
||||
Errors are plain messages on standard error, without PowerShell error blocks.
|
||||
.PARAMETER DryRun
|
||||
Enumerate, validate size/readability, and Base64-encode files without any
|
||||
network requests or delays. Requires a key but cannot validate it remotely.
|
||||
Honors Recurse and Silent. Does not validate actual image contents.
|
||||
.EXAMPLE
|
||||
$env:IMGBB_API_KEY = 'YOUR_API_KEY'
|
||||
.\Upload-ImagesToImgBB.ps1 -DirectoryPath 'C:\Images'
|
||||
.EXAMPLE
|
||||
.\Upload-ImagesToImgBB.ps1 -DirectoryPath './images' -ApiKey $key -Recurse
|
||||
.EXAMPLE
|
||||
.\Upload-ImagesToImgBB.ps1 -DirectoryPath './images' -Recurse -DryRun
|
||||
.EXAMPLE
|
||||
.\Upload-ImagesToImgBB.ps1 -DirectoryPath './images' -Silent
|
||||
.EXAMPLE
|
||||
.\Upload-ImagesToImgBB.ps1 -DirectoryPath './images' -WaitTime Off
|
||||
Disables ordinary request delays while retaining retry waits.
|
||||
.NOTES
|
||||
Intended for Windows PowerShell 5.1 and PowerShell 7.
|
||||
Exit codes: 0 = success (including no matching files); 1 = any failure.
|
||||
A run stopped early reports only files processed before the stop.
|
||||
No automatic expiration is requested. No files are modified locally.
|
||||
Re-running uploads files again. A lost response followed by a retry can also
|
||||
create a duplicate: the API does not provide an idempotency key here.
|
||||
Do not publish real keys, private images, or transcripts containing secrets.
|
||||
.LINK
|
||||
https://api.imgbb.com/
|
||||
#>
|
||||
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2026 Upload-ImagesToImgBB contributors
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$DirectoryPath,
|
||||
|
||||
[string]$ApiKey,
|
||||
[AllowNull()]
|
||||
[AllowEmptyString()]
|
||||
[string]$WaitTime = 'Random',
|
||||
[switch]$Recurse,
|
||||
[switch]$Silent,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
function Write-Status {
|
||||
param (
|
||||
[string]$Message,
|
||||
[ConsoleColor]$Color = [ConsoleColor]::Cyan
|
||||
)
|
||||
if (-not $Silent) { Write-Host $Message -ForegroundColor $Color }
|
||||
}
|
||||
|
||||
function Write-UploadError {
|
||||
param (
|
||||
[string]$Message,
|
||||
[ConsoleColor]$Color = [ConsoleColor]::Red
|
||||
)
|
||||
# Do not print raw exceptions, request bodies, or server response bodies.
|
||||
# Preserve standard error and plain messages without PowerShell error blocks.
|
||||
# Redirected errors receive no color sequences; unsupported hosts use plain text.
|
||||
$restoreColor = $false
|
||||
try {
|
||||
if (-not [Console]::IsErrorRedirected) {
|
||||
$previousColor = [Console]::ForegroundColor
|
||||
[Console]::ForegroundColor = $Color
|
||||
$restoreColor = $true
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
try {
|
||||
[Console]::Error.WriteLine("[ERROR] $Message")
|
||||
}
|
||||
finally {
|
||||
if ($restoreColor) {
|
||||
try { [Console]::ForegroundColor = $previousColor }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RetryAfterSeconds {
|
||||
param ($HttpResponse)
|
||||
if ($null -eq $HttpResponse) { return 0.0 }
|
||||
try {
|
||||
# Both WebHeaderCollection (5.1) and HttpResponseHeaders (7) support this.
|
||||
$headerValues = @($HttpResponse.Headers.GetValues('Retry-After'))
|
||||
if ($headerValues.Count -eq 0) { return 0.0 }
|
||||
$header = [string]$headerValues[0]
|
||||
$seconds = 0L
|
||||
if ([long]::TryParse($header, [ref]$seconds) -and $seconds -ge 0) {
|
||||
return [double]$seconds
|
||||
}
|
||||
$date = [DateTimeOffset]::MinValue
|
||||
if ([DateTimeOffset]::TryParse($header,
|
||||
[Globalization.CultureInfo]::InvariantCulture,
|
||||
[Globalization.DateTimeStyles]::AssumeUniversal, [ref]$date)) {
|
||||
return [Math]::Max(0, [Math]::Ceiling(($date - [DateTimeOffset]::UtcNow).TotalSeconds))
|
||||
}
|
||||
}
|
||||
catch { } # Missing/malformed headers fall back to exponential backoff.
|
||||
return 0.0
|
||||
}
|
||||
|
||||
function Wait-UploadDelay {
|
||||
param ([double]$Seconds)
|
||||
# Chunk long Retry-After values without shortening the requested wait.
|
||||
while ($Seconds -gt 0) {
|
||||
$chunk = [int][Math]::Min(60, [Math]::Ceiling($Seconds))
|
||||
Start-Sleep -Seconds $chunk
|
||||
$Seconds -= $chunk
|
||||
}
|
||||
}
|
||||
|
||||
$finalApiKey = if ([string]::IsNullOrWhiteSpace($ApiKey)) {
|
||||
$env:IMGBB_API_KEY
|
||||
}
|
||||
else { $ApiKey }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($finalApiKey)) {
|
||||
Write-UploadError 'API key missing. Use -ApiKey or set IMGBB_API_KEY.'
|
||||
exit 1
|
||||
}
|
||||
|
||||
$processed = 0L
|
||||
$succeeded = 0L
|
||||
$failed = 0L
|
||||
$runFailed = $false
|
||||
$requestSent = $false
|
||||
# Deliberately avoid ValidateSet: unknown values must fall back to random waits.
|
||||
$useRandomWait = $WaitTime -ine 'Off'
|
||||
$maxAttempts = 4 # One initial request plus three retries.
|
||||
$transientStatusCodes = @(408, 429, 500, 502, 503, 504)
|
||||
$endpoint = 'https://api.imgbb.com/1/upload'
|
||||
$originalSecurityProtocol = [Net.ServicePointManager]::SecurityProtocol
|
||||
|
||||
try {
|
||||
$directory = Get-Item -LiteralPath $DirectoryPath -ErrorAction Stop
|
||||
if (-not $directory.PSIsContainer -or $directory.PSProvider.Name -ne 'FileSystem') {
|
||||
throw 'A filesystem directory is required.'
|
||||
}
|
||||
|
||||
if (-not $DryRun -and $PSVersionTable.PSVersion.Major -eq 5) {
|
||||
[Net.ServicePointManager]::SecurityProtocol = $originalSecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
||||
}
|
||||
|
||||
$mode = if ($DryRun) { 'Dry run' } else { 'Upload' }
|
||||
Write-Status "$mode started. Recursive search: $([bool]$Recurse)."
|
||||
if (-not $DryRun) {
|
||||
$waitMode = if ($useRandomWait) { 'Random (3-6 seconds)' } else { 'Off (retry waits still apply)' }
|
||||
Write-Status "Request delay: $waitMode."
|
||||
}
|
||||
|
||||
Get-ChildItem -LiteralPath $directory.FullName -File -Recurse:$Recurse -ErrorAction Stop |
|
||||
Where-Object { $_.Extension -in @('.jpg', '.jpeg', '.png') } |
|
||||
ForEach-Object {
|
||||
$image = $_
|
||||
$processed++
|
||||
$base64Image = $null
|
||||
try {
|
||||
# Check again after reading in case a file changed during enumeration.
|
||||
if ($image.Length -eq 0 -or $image.Length -gt 32MB) {
|
||||
$failed++
|
||||
Write-UploadError "[SKIP] '$($image.FullName)': size $($image.Length) bytes; allowed range is 1 byte to 32 MiB." -Color Yellow
|
||||
return
|
||||
}
|
||||
$fileBytes = [IO.File]::ReadAllBytes($image.FullName)
|
||||
if ($fileBytes.Length -eq 0 -or $fileBytes.Length -gt 32MB) {
|
||||
$failed++
|
||||
Write-UploadError "[SKIP] '$($image.FullName)': size after reading is $($fileBytes.Length) bytes; allowed range is 1 byte to 32 MiB." -Color Yellow
|
||||
return
|
||||
}
|
||||
$base64Image = [Convert]::ToBase64String($fileBytes)
|
||||
}
|
||||
catch {
|
||||
$failed++
|
||||
$reason = $_.Exception.GetBaseException().GetType().Name
|
||||
Write-UploadError "[SKIP] '$($image.FullName)': cannot read or encode the file ($reason)." -Color Yellow
|
||||
return # Skip this pipeline item; continue with the next file.
|
||||
}
|
||||
finally { $fileBytes = $null }
|
||||
|
||||
if ($DryRun) {
|
||||
$succeeded++
|
||||
Write-Status "[DRY RUN] Would upload: $($image.FullName)"
|
||||
$base64Image = $null
|
||||
return
|
||||
}
|
||||
|
||||
$body = @{ key = $finalApiKey; image = $base64Image; name = $image.BaseName }
|
||||
try {
|
||||
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
|
||||
if ($requestSent -and $useRandomWait) {
|
||||
Wait-UploadDelay -Seconds (Get-Random -Minimum 3 -Maximum 7)
|
||||
}
|
||||
Write-Status "[UPLOAD $attempt/$maxAttempts] $($image.FullName)" -Color White
|
||||
$statusCode = 0
|
||||
$retryAfter = 0.0
|
||||
$requestSent = $true
|
||||
try {
|
||||
$response = Invoke-RestMethod -Uri $endpoint -Method Post -Body $body `
|
||||
-ContentType 'application/x-www-form-urlencoded' -TimeoutSec 60 `
|
||||
-MaximumRedirection 0 -UseBasicParsing -ErrorAction Stop -Verbose:$false -Debug:$false
|
||||
if ($response.success -eq $true) {
|
||||
$succeeded++
|
||||
Write-Status "[SUCCESS] $($image.FullName) -> $($response.data.url)" -Color Green
|
||||
break
|
||||
}
|
||||
# An unsuccessful HTTP 2xx response must also terminate or retry.
|
||||
if ($null -ne $response.status) {
|
||||
$parsedStatus = 0
|
||||
if ([int]::TryParse([string]$response.status, [ref]$parsedStatus)) {
|
||||
$statusCode = $parsedStatus
|
||||
}
|
||||
}
|
||||
if ($statusCode -eq 0) { $statusCode = 400 }
|
||||
}
|
||||
catch {
|
||||
$httpResponse = $_.Exception.Response
|
||||
if ($null -ne $httpResponse) {
|
||||
$statusCode = [int]$httpResponse.StatusCode
|
||||
$retryAfter = Get-RetryAfterSeconds -HttpResponse $httpResponse
|
||||
}
|
||||
# Status 0 represents a transport failure with no HTTP response.
|
||||
}
|
||||
|
||||
$transient = $statusCode -eq 0 -or $statusCode -in $transientStatusCodes
|
||||
$label = if ($statusCode -eq 0) { 'transport failure' } else { "HTTP/API $statusCode" }
|
||||
Write-UploadError "Attempt $attempt/$maxAttempts failed for '$($image.FullName)' ($label)."
|
||||
|
||||
if ($transient -and $attempt -lt $maxAttempts) {
|
||||
$backoff = 5 * [Math]::Pow(2, $attempt - 1) + (Get-Random -Minimum 0 -Maximum 4)
|
||||
$wait = [Math]::Max($backoff, $retryAfter)
|
||||
Write-Status "[RETRY] Waiting at least $wait seconds." -Color Yellow
|
||||
Wait-UploadDelay -Seconds $wait
|
||||
continue
|
||||
}
|
||||
|
||||
$failed++
|
||||
if ($statusCode -in @(401, 403) -or $transient) {
|
||||
throw 'Authentication/access failure or exhausted transient retries.'
|
||||
}
|
||||
# A permanent per-file rejection should not retry unchanged input.
|
||||
break
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$body.Clear()
|
||||
$base64Image = $null
|
||||
$response = $null
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$runFailed = $true
|
||||
Write-UploadError 'Run stopped. Check the directory and its permissions; if requests failed, check the API key, connection, or service availability before restarting.'
|
||||
}
|
||||
finally {
|
||||
if ($PSVersionTable.PSVersion.Major -eq 5) {
|
||||
[Net.ServicePointManager]::SecurityProtocol = $originalSecurityProtocol
|
||||
}
|
||||
$finalApiKey = $null
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Status "Dry run finished. Processed: $processed; ready: $succeeded; failed: $failed."
|
||||
}
|
||||
else {
|
||||
Write-Status "Upload finished. Processed: $processed; uploaded: $succeeded; failed: $failed."
|
||||
}
|
||||
if ($runFailed -or $failed -gt 0) { exit 1 }
|
||||
exit 0
|
||||
Reference in New Issue
Block a user