Skip to content

Commit c366cf8

Browse files
Added initial ConvertApi ps module, readme, examples, ignore and gitaction.
1 parent 2028df6 commit c366cf8

File tree

7 files changed

+618
-1
lines changed

7 files changed

+618
-1
lines changed

.github/workflows/ci.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
pwsh:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout
12+
uses: actions/checkout@v4
13+
14+
- name: PowerShell version
15+
run: pwsh -c '$PSVersionTable | Format-List *'
16+
17+
- name: Install PSScriptAnalyzer
18+
run: pwsh -c 'Install-Module PSScriptAnalyzer -Scope CurrentUser -Force'
19+
20+
- name: Lint module
21+
run: pwsh -c 'Invoke-ScriptAnalyzer -Path ./ConvertApi -Recurse -Severity Warning -ReportSummary'
22+
23+
- name: Import test
24+
run: pwsh -c 'Import-Module ./ConvertApi/ConvertApi.psd1 -Force; (Get-Command Invoke-ConvertApi).Name; Get-Help Invoke-ConvertApi -ErrorAction SilentlyContinue | Out-Null'

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# editors & OS
2+
.vscode/
3+
.idea/
4+
.DS_Store
5+
6+
# build/output/temp
7+
out/
8+
dist/
9+
bin/
10+
obj/
11+
*.nupkg
12+
*.log
13+
14+
# never commit secrets
15+
*.secret
16+
*.env

ConvertApi/ConvertApi.psd1

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@{
2+
RootModule = 'ConvertApi.psm1'
3+
ModuleVersion = '1.0.0'
4+
GUID = '8f28c2d0-0f4d-4d64-9f65-2a0a8e0e9b10'
5+
Author = 'Your Name or Company'
6+
CompanyName = 'Your Company'
7+
Description = 'Thin PowerShell wrapper for ConvertAPI v2 REST using CONVERTAPI_API_TOKEN; file/URL input, extra params, safe downloads.'
8+
PowerShellVersion = '5.1'
9+
CompatiblePSEditions = @('Desktop','Core')
10+
FunctionsToExport = @('Invoke-ConvertApi','Set-ConvertApiToken','Get-ConvertApiToken')
11+
CmdletsToExport = @()
12+
VariablesToExport = @()
13+
AliasesToExport = @()
14+
PrivateData = @{
15+
PSData = @{
16+
Tags = @('convertapi','conversion','pdf','docx','automation','powershell','token')
17+
}
18+
}
19+
}

ConvertApi/ConvertApi.psm1

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
#requires -Version 5.1
2+
# Public functions: Invoke-ConvertApi, Set-ConvertApiToken, Get-ConvertApiToken
3+
4+
$script:ConvertApiToken = $null
5+
$script:ConvertApiAuthUrl = 'https://www.convertapi.com/a/authentication'
6+
7+
function Get-ConvertApiToken {
8+
<#
9+
.SYNOPSIS
10+
Gets the ConvertAPI API token from memory or environment.
11+
12+
.DESCRIPTION
13+
Returns the in-session token if set, or the CONVERTAPI_API_TOKEN environment variable.
14+
15+
.LINK
16+
https://www.convertapi.com/a/authentication
17+
#>
18+
[CmdletBinding()]
19+
param()
20+
if ($script:ConvertApiToken) { return $script:ConvertApiToken }
21+
if ($env:CONVERTAPI_API_TOKEN) { return $env:CONVERTAPI_API_TOKEN }
22+
return $null
23+
}
24+
25+
function Set-ConvertApiToken {
26+
<#
27+
.SYNOPSIS
28+
Sets the ConvertAPI API token for this session (and optionally persists it).
29+
30+
.EXAMPLE
31+
Set-ConvertApiToken 'YOUR_API_TOKEN' -Persist
32+
33+
.NOTES
34+
Get your API token from: https://www.convertapi.com/a/authentication
35+
36+
.LINK
37+
https://www.convertapi.com/a/authentication
38+
#>
39+
[CmdletBinding(SupportsShouldProcess)]
40+
param(
41+
[Parameter(Mandatory)][string]$Token,
42+
[switch]$Persist
43+
)
44+
$script:ConvertApiToken = $Token
45+
if ($Persist) {
46+
if ($PSCmdlet.ShouldProcess("Environment", "Set CONVERTAPI_API_TOKEN (User scope)")) {
47+
[Environment]::SetEnvironmentVariable("CONVERTAPI_API_TOKEN", $Token, "User")
48+
}
49+
}
50+
}
51+
52+
function Invoke-ConvertApi {
53+
<#
54+
.SYNOPSIS
55+
Converts (or merges) files via ConvertAPI v2 REST, supporting multiple files/URLs on PS 5.1.
56+
57+
.DESCRIPTION
58+
- Single local file => uploads raw bytes with Content-Disposition.
59+
- Multiple files/URLs => HttpClient multipart with Files[0], Files[1], ...
60+
- Single URL => passes ?Url=... as a query parameter.
61+
- Extra API params via -Parameters.
62+
- Use -StoreFile to get time-limited URLs for downloading.
63+
64+
.EXAMPLE
65+
Invoke-ConvertApi -From pdf -To merge -File .\a.pdf, .\b.pdf -OutputPath .\out -StoreFile
66+
67+
.LINK
68+
https://www.convertapi.com/a/authentication
69+
#>
70+
[CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'LocalFile')]
71+
param(
72+
[Parameter(Mandatory)][ValidatePattern('^[A-Za-z0-9_-]+$')] [string]$From,
73+
[Parameter(Mandatory)][ValidatePattern('^[A-Za-z0-9_-]+$')] [string]$To,
74+
75+
# Local file(s)
76+
[Parameter(ParameterSetName='LocalFile', ValueFromPipeline, ValueFromPipelineByPropertyName)]
77+
[Alias('FullName','Path')] [string[]]$File,
78+
79+
# Remote URL(s)
80+
[Parameter(ParameterSetName='RemoteUrl')]
81+
[string[]]$Url,
82+
83+
[string]$OutputPath = (Get-Location).Path,
84+
[hashtable]$Parameters,
85+
[switch]$StoreFile, # adds ?StoreFile=true or form field StoreFile=true
86+
[string]$Token, # overrides env/module token
87+
[int]$TimeoutSec = 300,
88+
[switch]$Overwrite,
89+
[switch]$PassThru
90+
)
91+
92+
begin {
93+
if (-not $Token) { $Token = Get-ConvertApiToken }
94+
if (-not $Token) {
95+
$auth = $script:ConvertApiAuthUrl
96+
throw ("No API token found. Get your token at: {0}`n" +
97+
"Then run: Set-ConvertApiToken 'YOUR_API_TOKEN' -Persist`n" +
98+
"Or set: $env:CONVERTAPI_API_TOKEN") -f $auth
99+
}
100+
101+
if (-not (Test-Path $OutputPath)) {
102+
New-Item -ItemType Directory -Path $OutputPath | Out-Null
103+
}
104+
105+
$headers = @{
106+
"Authorization" = "Bearer $Token"
107+
"Accept" = "application/json"
108+
}
109+
110+
# Prefer TLS 1.2 on Windows PowerShell
111+
if ($PSVersionTable.PSEdition -eq 'Desktop') {
112+
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}
113+
}
114+
115+
function New-ConvertApiUri([string]$From,[string]$To,[hashtable]$Parms){
116+
$base = "https://v2.convertapi.com/convert/{0}/to/{1}" -f $From, $To
117+
if (-not $Parms -or $Parms.Count -eq 0) { return $base }
118+
$qs = ($Parms.GetEnumerator() | ForEach-Object {
119+
$v = $_.Value; if ($v -is [bool]) { $v = $v.ToString().ToLower() }
120+
'{0}={1}' -f [uri]::EscapeDataString($_.Key), [uri]::EscapeDataString([string]$v)
121+
}) -join '&'
122+
return "$base`?$qs"
123+
}
124+
125+
function Save-ConvertApiFiles($response) {
126+
foreach ($f in $response.Files) {
127+
$target = Join-Path $OutputPath $f.FileName
128+
if ((Test-Path $target) -and -not $Overwrite) {
129+
$target = Join-Path $OutputPath ("{0}-{1}{2}" -f [IO.Path]::GetFileNameWithoutExtension($f.FileName), (Get-Random), [IO.Path]::GetExtension($f.FileName))
130+
}
131+
Invoke-WebRequest -Uri $f.Url -OutFile $target | Out-Null
132+
Write-Verbose "Saved $target"
133+
if ($PassThru) { Get-Item $target }
134+
}
135+
}
136+
137+
# PS 5.1-safe multipart using HttpClient
138+
function Invoke-ConvertApiMultipart([string]$Uri, [hashtable]$FilesTable, [hashtable]$UrlsTable, [hashtable]$Fields, [string]$Token, [int]$TimeoutSec){
139+
Add-Type -AssemblyName System.Net.Http
140+
141+
$handler = New-Object System.Net.Http.HttpClientHandler
142+
$client = New-Object System.Net.Http.HttpClient($handler)
143+
$client.Timeout = [TimeSpan]::FromSeconds($TimeoutSec)
144+
145+
$content = New-Object System.Net.Http.MultipartFormDataContent
146+
$streams = @()
147+
148+
try {
149+
# Add files
150+
foreach ($kv in $FilesTable.GetEnumerator()){
151+
$path = $kv.Value
152+
$stream = [System.IO.File]::OpenRead($path)
153+
$streams += $stream
154+
$sc = New-Object System.Net.Http.StreamContent($stream)
155+
$sc.Headers.ContentDisposition = New-Object System.Net.Http.Headers.ContentDispositionHeaderValue("form-data")
156+
$sc.Headers.ContentDisposition.Name = '"' + $kv.Key + '"'
157+
$sc.Headers.ContentDisposition.FileName = '"' + [IO.Path]::GetFileName($path) + '"'
158+
$content.Add($sc)
159+
}
160+
161+
# Add URLs as string fields (Files[i] = 'https://...')
162+
foreach ($kv in $UrlsTable.GetEnumerator()){
163+
$content.Add([System.Net.Http.StringContent]::new($kv.Value), $kv.Key)
164+
}
165+
166+
# Add extra fields (StoreFile, Parameters...)
167+
foreach ($kv in $Fields.GetEnumerator()){
168+
$content.Add([System.Net.Http.StringContent]::new([string]$kv.Value), $kv.Key)
169+
}
170+
171+
$req = New-Object System.Net.Http.HttpRequestMessage([System.Net.Http.HttpMethod]::Post, $Uri)
172+
$req.Content = $content
173+
$req.Headers.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", $Token)
174+
$req.Headers.Accept.Add([System.Net.Http.Headers.MediaTypeWithQualityHeaderValue]::new("application/json"))
175+
176+
$resp = $client.SendAsync($req).Result
177+
$resp.EnsureSuccessStatusCode() | Out-Null
178+
$json = $resp.Content.ReadAsStringAsync().Result
179+
180+
return $json | ConvertFrom-Json
181+
}
182+
finally {
183+
foreach ($s in $streams) { try { $s.Dispose() } catch {} }
184+
try { $content.Dispose() } catch {}
185+
try { $client.Dispose() } catch {}
186+
}
187+
}
188+
189+
# Common query/form params
190+
$common = @{}
191+
if ($StoreFile.IsPresent) { $common["StoreFile"] = "true" }
192+
if ($Parameters) { foreach ($k in $Parameters.Keys) { $common[$k] = $Parameters[$k] } }
193+
}
194+
195+
process {
196+
$fileCount = @($File).Count
197+
$urlCount = @($Url).Count
198+
if ($fileCount -eq 0 -and $urlCount -eq 0) {
199+
throw "Provide at least one -File or -Url."
200+
}
201+
202+
# MULTI-INPUT (merge etc.) via HttpClient multipart —— PS 5.1 safe
203+
if ($fileCount + $urlCount -gt 1) {
204+
$uri = New-ConvertApiUri -From $From -To $To -Parms $null
205+
206+
# Build Files[i] in order
207+
$filesTable = [ordered]@{}
208+
$urlsTable = [ordered]@{}
209+
$i = 0
210+
foreach ($p in $File) {
211+
if (-not (Test-Path $p)) { throw "Input not found: $p" }
212+
$resolved = (Resolve-Path $p).Path
213+
$filesTable["Files[$i]"] = $resolved
214+
$i++
215+
}
216+
foreach ($u in $Url) {
217+
$urlsTable["Files[$i]"] = $u
218+
$i++
219+
}
220+
221+
# Extra fields
222+
$fields = [ordered]@{}
223+
foreach ($k in $common.Keys) {
224+
$v = $common[$k]; if ($v -is [bool]) { $v = $v.ToString().ToLower() }
225+
$fields[$k] = [string]$v
226+
}
227+
228+
$label = ("{0} item(s)" -f ($fileCount + $urlCount))
229+
if ($PSCmdlet.ShouldProcess($label, "Convert $From -> $To (multipart)")) {
230+
$response = Invoke-ConvertApiMultipart -Uri $uri -FilesTable $filesTable -UrlsTable $urlsTable -Fields $fields -Token $Token -TimeoutSec $TimeoutSec
231+
Save-ConvertApiFiles $response
232+
}
233+
return
234+
}
235+
236+
# SINGLE URL
237+
if ($urlCount -eq 1 -and $fileCount -eq 0) {
238+
$q = $common.Clone(); $q["Url"] = $Url[0]
239+
$uri = New-ConvertApiUri -From $From -To $To -Parms $q
240+
if ($PSCmdlet.ShouldProcess($Url[0], "Convert $From -> $To (url)")) {
241+
$response = Invoke-RestMethod -Uri $uri -Method POST -Headers $headers -TimeoutSec $TimeoutSec
242+
Save-ConvertApiFiles $response
243+
}
244+
return
245+
}
246+
247+
# SINGLE LOCAL FILE (octet-stream)
248+
if ($fileCount -eq 1) {
249+
$resolved = (Resolve-Path $File[0]).Path
250+
$name = [IO.Path]::GetFileName($resolved)
251+
$bytes = [IO.File]::ReadAllBytes($resolved)
252+
253+
$uri = New-ConvertApiUri -From $From -To $To -Parms $common
254+
$localHeaders = $headers.Clone()
255+
$localHeaders["Content-Type"] = "application/octet-stream"
256+
$localHeaders["Content-Disposition"] = "attachment; filename=`"$name`""
257+
258+
if ($PSCmdlet.ShouldProcess($name, "Convert $From -> $To (single file)")) {
259+
$response = Invoke-RestMethod -Uri $uri -Method POST -Headers $localHeaders -Body $bytes -TimeoutSec $TimeoutSec
260+
Save-ConvertApiFiles $response
261+
}
262+
return
263+
}
264+
}
265+
}
266+
267+
Export-ModuleMember -Function Invoke-ConvertApi, Set-ConvertApiToken, Get-ConvertApiToken

0 commit comments

Comments
 (0)