diff --git a/CHANGELOG.md b/CHANGELOG.md index 3579c69..7013c14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [0.0.5] - 2026-04-03 + +### Changed + - Updated Send-BoxFile Function to have the ability to send files from memory. + - Updated Invoke-BoxRestCall to support Form Settings + ## [0.0.4] - 2026-03-31 - Added the following functions; Get-BoxCollaboration, Set-Box Collaboration, Remove-BoxCollaboration. diff --git a/src/UofIBox/UofIBox.psd1 b/src/UofIBox/UofIBox.psd1 index 59a27eb..c0f02e4 100644 --- a/src/UofIBox/UofIBox.psd1 +++ b/src/UofIBox/UofIBox.psd1 @@ -10,7 +10,7 @@ RootModule = 'UofIBox.psm1' # Version number of this module. -ModuleVersion = '1.0.1' +ModuleVersion = '1.0.2' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/src/UofIBox/UofIBox.psm1 b/src/UofIBox/UofIBox.psm1 index 29b9165..d52cd77 100644 --- a/src/UofIBox/UofIBox.psm1 +++ b/src/UofIBox/UofIBox.psm1 @@ -1,3 +1,4 @@ +$Script:Settings = Get-Content -Path "$PSScriptRoot\settings.json" | ConvertFrom-Json $Script:BoxSession = $NULL [int]$Script:APICallCount = 0 diff --git a/src/UofIBox/dscresources/.gitkeep b/src/UofIBox/dscresources/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 b/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 index 47cadec..4318523 100644 --- a/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 +++ b/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 @@ -19,6 +19,9 @@ Hashtable body that will be converted to JSON. .PARAMETER Upload Switch indicating the upload API endpoint should be used. +.PARAMETER Form +Hashtable of form fields for multipart form data requests. + .EXAMPLE Invoke-BoxRestCall -RelativeURI "folders/123" -Method GET @@ -38,6 +41,8 @@ function Invoke-BoxRestCall { [hashtable]$Body, + [hashtable]$Form, + [switch]$Upload ) @@ -52,10 +57,12 @@ function Invoke-BoxRestCall { } if ($Upload) { - $BaseURI = "https://upload.box.com/api/2.0/" + $BaseURI = "$($Script:Settings.UploadBaseURI)" + $contentType = "multipart/form-data" } else { - $BaseURI = "https://api.box.com/2.0/" + $BaseURI = "$($Script:Settings.BaseURI)" + $contentType = "application/json" } } @@ -64,7 +71,7 @@ function Invoke-BoxRestCall { $IVRSplat = @{ Headers = @{ Authorization = "Bearer $($Script:BoxSession.AccessToken)" - "Content-Type" = "application/json" + "Content-Type" = $contentType } Method = $Method Uri = "$BaseURI$RelativeURI" @@ -74,6 +81,10 @@ function Invoke-BoxRestCall { $IVRSplat.Add("Body", ($Body | ConvertTo-Json -Depth 10)) } + if ($Form) { + $IVRSplat.Add("Form", $Form) + } + Write-Verbose "Calling Box API: $Method $RelativeURI" try { diff --git a/src/UofIBox/functions/public/Send-BoxFile.ps1 b/src/UofIBox/functions/public/Send-BoxFile.ps1 index 1342244..504cd17 100644 --- a/src/UofIBox/functions/public/Send-BoxFile.ps1 +++ b/src/UofIBox/functions/public/Send-BoxFile.ps1 @@ -1,54 +1,117 @@ <# .SYNOPSIS -Uploads a file to Box. +Uploads a file to Box from a local path or from memory. .DESCRIPTION -Uploads a local file to a specified Box folder. +Uploads a file to a specified Box folder. You can upload a file directly from a local path +or from an in-memory byte array, which is useful for generating files on the fly +without writing to disk. .PARAMETER FilePath -Local path of the file to upload. +(Local path upload) The path of the file on the local filesystem to upload. +Required when uploading from a file. + +.PARAMETER FileBytes +(In-memory upload) The file content as a byte array. Required when uploading from memory. + +.PARAMETER FileName +(In-memory upload) The name to give the file in Box. Required when using FileBytes. .PARAMETER ParentFolderId -Box folder ID where the file will be uploaded. +The Box folder ID where the file should be uploaded. .EXAMPLE +# Upload a local file Send-BoxFile -FilePath "C:\Temp\report.pdf" -ParentFolderId "123456" + +.EXAMPLE +# Upload a CSV from memory +$csv = $data | ConvertTo-Csv -NoTypeInformation +$bytes = [System.Text.Encoding]::UTF8.GetBytes($csv -join "`n") +Send-BoxFile -FileBytes $bytes -FileName "report.csv" -ParentFolderId "123456" #> function Send-BoxFile { - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = "FromPath")] param( - [Parameter(Mandatory)] + # --- File Path Upload --- + [Parameter(Mandatory, ParameterSetName = "FromPath")] [string]$FilePath, + # --- In-Memory Upload --- + [Parameter(Mandatory, ParameterSetName = "FromMemory")] + [byte[]]$FileBytes, + + [Parameter(Mandatory, ParameterSetName = "FromMemory")] + [string]$FileName, + + # --- Shared --- [Parameter(Mandatory)] [string]$ParentFolderId ) - if ($null -eq $Script:BoxSession) { - throw "No Box session established. Run New-BoxSession first." + switch ($PSCmdlet.ParameterSetName) { + + "FromPath" { + if (-not (Test-Path -Path $FilePath -PathType Leaf)) { + throw "File not found at path: $FilePath" + } + + $FileBytes = [System.IO.File]::ReadAllBytes($FilePath) + $FileName = [System.IO.Path]::GetFileName($FilePath) + } + + "FromMemory" { + } } $Attributes = @{ - name = [System.IO.Path]::GetFileName($FilePath) + name = $FileName parent = @{ id = $ParentFolderId } } | ConvertTo-Json -Compress - $Headers = @{ - Authorization = "Bearer $($Script:BoxSession.AccessToken)" - } + # --- Build multipart form body --- + $boundary = [System.Guid]::NewGuid().ToString() + $LF = "`r`n" + + $memStream = New-Object System.IO.MemoryStream + $writer = New-Object System.IO.StreamWriter($memStream) + + # --- attributes part --- + $writer.Write("--$boundary$LF") + $writer.Write("Content-Disposition: form-data; name=`"attributes`"$LF") + $writer.Write("Content-Type: application/json$LF$LF") + $writer.Write($Attributes + $LF) - $Form = @{ - attributes = $Attributes - file = Get-Item $FilePath + # --- file part --- + $writer.Write("--$boundary$LF") + $writer.Write("Content-Disposition: form-data; name=`"file`"; filename=`"$FileName`"$LF") + $writer.Write("Content-Type: application/octet-stream$LF$LF") + $writer.Flush() + + # Write raw file bytes + $memStream.Write($FileBytes, 0, $FileBytes.Length) + + # --- closing boundary --- + $writer.Write("$LF--$boundary--$LF") + $writer.Flush() + + $bodyBytes = $memStream.ToArray() + $memStream.Close() + + # --- Invoke API --- + $IVRSplat = @{ + Uri = "$($Script:Settings.UploadBaseURI)files/content" + Method = "POST" + Headers = @{ + Authorization = "Bearer $($Script:BoxSession.AccessToken)" + "Content-Type" = "multipart/form-data; boundary=$boundary" + } + Body = $bodyBytes } - Invoke-RestMethod ` - -Method POST ` - -Uri "$($Script:Settings.UploadBaseURI)files/content" ` - -Headers $Headers ` - -Form $Form + Invoke-RestMethod @IVRSplat } diff --git a/src/UofIBox/settings.json b/src/UofIBox/settings.json new file mode 100644 index 0000000..1171bf2 --- /dev/null +++ b/src/UofIBox/settings.json @@ -0,0 +1,4 @@ +{ + "BaseURI": ["https://api.box.com/2.0/"], + "UploadBaseURI": ["https://upload.box.com/api/2.0/"] +}