diff --git a/.github/workflows/pester.yml b/.github/workflows/pester.yml new file mode 100644 index 0000000..7608511 --- /dev/null +++ b/.github/workflows/pester.yml @@ -0,0 +1,20 @@ +name: Pester Tests + +on: [pull_request] + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + run: Install-Module -Name 'Pester' -Force -SkipPublisherCheck + shell: pwsh + - name: Run tests + run: Invoke-Pester -Path './test/' -CI + shell: pwsh diff --git a/.github/workflows/scriptanalyzer.yml b/.github/workflows/scriptanalyzer.yml new file mode 100644 index 0000000..d1597cd --- /dev/null +++ b/.github/workflows/scriptanalyzer.yml @@ -0,0 +1,21 @@ +name: ScriptAnalyzer + +on: [pull_request] + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest] + + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + run: Install-Module -Name 'PSScriptAnalyzer' -Force -SkipPublisherCheck + shell: pwsh + - name: ScriptAnalyzer + run: Invoke-ScriptAnalyzer -Path '.\src\' -Recurse -EnableExit + shell: pwsh + diff --git a/CHANGELOG.md b/CHANGELOG.md index eafbb04..4bc22af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - + ## Unreleased ### Added @@ -13,3 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed ### Removed + +## [0.0.1] - 2026-03-17 + +### Added + + - Initial Push, first batch of functions added to the module. diff --git a/LICENSE b/LICENSE index 23fa39a..d82e321 100644 --- a/LICENSE +++ b/LICENSE @@ -1,35 +1,29 @@ -University of Illinois/NCSA Open Source License +Copyright (c) 2026 University of Illinois. All rights reserved. -Copyright (c) 2023 by the Board of Trustees of the University of -Illinois. All rights reserved. -Developed by Cybersecurity and Technology Services -University of Illinois -https://techservices.illinois.edu +Developed by: Cybersecurity Engineering + University of Illinois + https://github.com/techservicesillinois/Secops-Powershell-Box -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation files -(the "Software"), to deal with 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: - -- Redistributions of source code must retain the above copyright notice, +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +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: +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the documentation + and/or other materials provided with the distribution. +* Neither the names of Cybersecurity Engineering, University of Illinois, + nor the names of its contributors may be used to endorse or promote products + derived from this Software without specific prior written permission. -- Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimers in the - documentation and/or other materials provided with the distribution. - -- Neither the names of University of Illinois nor the names of its - contributors may be used to endorse or promote products derived from - this Software without specific prior written permission. - -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 +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 CONTRIBUTORS 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 WITH -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. diff --git a/README.md b/README.md index 18987b7..712552b 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,112 @@ -## About +![Pester Tests](https://github.com/techservicesillinois/Secops-Powershell-Box/workflows/Pester%20Tests/badge.svg) +![ScriptAnalyzer](https://github.com/techservicesillinois/Secops-Powershell-Box/workflows/ScriptAnalyzer/badge.svg) -This repository is used by Cybersecurity operations teams at the -University of Illinois as a GitHub template. +# What is This? -This resource helps comply with University of Illinois -Cybersecurity standards - including [IT-07][it07], [IT08][it08], -and [IT13][it13]. +This is a PowerShell module for automating interactions with the Box API. It provides a clean, reusable set of functions for managing folders, files, and user access within Box. -[it07]: https://go.illinois.edu/secstd-IT07 -[it08]: https://go.illinois.edu/secstd-IT08 -[it13]: https://go.illinois.edu/secstd-IT13 +The module is designed for automation scenarios such as: -See [Cybersecurity Development on the Illinois Knowledge Base][kbsearch] -for information about our development standards. +Ticket-driven provisioning -[kbsearch]: https://answers.uillinois.edu/illinois/search.php?q=cybersecurity+developer&cat=0 +Scheduled jobs -The remainder of this `README.md` contains example text. +Azure Automation runbooks -## Data Sources +Security and compliance workflows -|Data Store|Data Type|Sensitivity|Notes| -|----------|---------|-----------|-----| +It simplifies Box API usage by handling authentication, REST calls, and common operations behind easy-to-use PowerShell functions. -## Endpoint Connections +# How do I install it? -|Endpoint|Purpose|Stage|Access| -|--------|-------|-----|------| +The latest stable release is always available via the PSGallery. -## Product Support +This will install on the local machine: -This product is supported by Cybersecurity teams at the -University of Illinois Urbana-Champaign on a best-effort basis. +Install-Module -Name 'UofIBox' -As of the last update to this README, the expected End-of-Life and -End-of-Support dates of this product are . +### Prerequisites -End-of-Life was decided upon based on these dependencies: +PowerShell 7+ - - - - +A Box application configured for Client Credentials Grant +Box Client ID, Client Secret, and Enterprise ID + +# How does it work? + +The module is built around two core components: + +Authentication + +New-BoxSession authenticates to Box using OAuth Client Credentials and stores the access token for reuse. + +$Credential = Get-Credential +New-BoxSession -Credential $Credential +REST Wrapper + +Invoke-BoxRestCall is a centralized wrapper for all API calls. It handles: + +Authentication headers + +Base URI construction + +JSON conversion + +Error handling + +File downloads + +Key Functions +Folder Management + +New-BoxFolderWithCollaboration – Create folders and assign users/roles + +Get-BoxFolder – Retrieve folder metadata + +Remove-BoxFolder – Delete folders + +File Management + +Upload-BoxFile – Upload files + +Get-BoxFile – Retrieve file metadata + +Remove-BoxFile – Delete files + +Receive-BoxFile – Download files + +Folder Download + +Receive-BoxFolder – Recursively downloads a folder and recreates the structure locally + +Example Workflow +$Credential = Get-Credential +New-BoxSession -Credential $Credential + +New-BoxFolderWithCollaboration ` + -FolderName "SecurityProject" ` + -Login user@company.com ` + -Role editor + +# How do I help? + +Contributions are welcome. You can help by: + +Reporting bugs or issues + +Suggesting new features + +Improving documentation + +Refactoring functions for performance or readability + +If contributing: + +Follow existing function structure and naming conventions + +Ensure all functions use Invoke-BoxRestCall + +Include comment-based help for all functions + +# To Do diff --git a/src/UofIBox/UofIBox.psd1 b/src/UofIBox/UofIBox.psd1 new file mode 100644 index 0000000..ce4f63e --- /dev/null +++ b/src/UofIBox/UofIBox.psd1 @@ -0,0 +1,141 @@ +# +# Module manifest for module 'UofIBox' +# +# Generated by: Cybersecurity Engineering +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'UofIBox.psm1' + +# Version number of this module. +ModuleVersion = '1.0.0' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '308d74d8-6717-4324-be7d-206fc0adc551' + +# Author of this module +Author = 'Cybersecurity Engineering' + +# Company or vendor of this module +CompanyName = 'The University of Illinois' + +# Copyright statement for this module +Copyright = 'The University of Illinois Board of Trustees' + +# Description of the functionality provided by this module +Description = 'Powershell Wrapper for the Box API' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.0' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = @( + 'Get-BoxFolderData,' + 'Get-BoxFileData', + 'Invoke-BoxRestCall', + 'New-BoxFolderCollaboration' + 'New-BoxSession', + 'Receive-BoxFile', + 'Receive-BoxFolder', + 'Remove-BoxFolder', + 'Remove-BoxFile' + 'Send-BoxFile' + ) + + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = @() + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'https://github.com/techservicesillinois/Secops-Powershell-Box/blob/master/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/techservicesillinois/Secops-Powershell-Box' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} diff --git a/src/UofIBox/UofIBox.psm1 b/src/UofIBox/UofIBox.psm1 new file mode 100644 index 0000000..29b9165 --- /dev/null +++ b/src/UofIBox/UofIBox.psm1 @@ -0,0 +1,9 @@ +$Script:BoxSession = $NULL +[int]$Script:APICallCount = 0 + +[String]$FunctionPath = Join-Path -Path $PSScriptRoot -ChildPath 'Functions' +#All function files are executed while only public functions are exported to the shell. +Get-ChildItem -Path $FunctionPath -Filter "*.ps1" -Recurse | ForEach-Object -Process { + Write-Verbose -Message "Importing $($_.BaseName)" + . $_.FullName | Out-Null +} diff --git a/src/UofIBox/dscresources/.gitkeep b/src/UofIBox/dscresources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/UofIBox/functions/private/.gitkeep b/src/UofIBox/functions/private/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/UofIBox/functions/public/.gitkeep b/src/UofIBox/functions/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/UofIBox/functions/public/Get-BoxFileData.ps1 b/src/UofIBox/functions/public/Get-BoxFileData.ps1 new file mode 100644 index 0000000..c09d984 --- /dev/null +++ b/src/UofIBox/functions/public/Get-BoxFileData.ps1 @@ -0,0 +1,29 @@ +<# +.SYNOPSIS +Retrieves metadata for a Box file. + +.DESCRIPTION +Returns file information including size, name, and owner. + +.PARAMETER FileId +The ID of the Box file. + +.EXAMPLE +Get-BoxFileData -FileId "123456789" +#> + +function Get-BoxFileData { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FileId + ) + + $Call = @{ + RelativeURI = "files/$FileId" + Method = "GET" + } + + Invoke-BoxRestCall @Call +} diff --git a/src/UofIBox/functions/public/Get-BoxFolderData.ps1 b/src/UofIBox/functions/public/Get-BoxFolderData.ps1 new file mode 100644 index 0000000..4135721 --- /dev/null +++ b/src/UofIBox/functions/public/Get-BoxFolderData.ps1 @@ -0,0 +1,29 @@ +<# +.SYNOPSIS +Retrieves Box folder metadata. + +.DESCRIPTION +Gets metadata information for a specified Box folder. + +.PARAMETER FolderId +The ID of the Box folder. + +.EXAMPLE +Get-BoxFolderData -FolderId "123456789" +#> + +function Get-BoxFolderData { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FolderId + ) + + $Call = @{ + RelativeURI = "folders/$FolderId" + Method = "GET" + } + + Invoke-BoxRestCall @Call +} diff --git a/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 b/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 new file mode 100644 index 0000000..47cadec --- /dev/null +++ b/src/UofIBox/functions/public/Invoke-BoxRestCall.ps1 @@ -0,0 +1,97 @@ +<# +.SYNOPSIS +Makes a REST API call to the Box API. + +.DESCRIPTION +Wrapper for Invoke-RestMethod that handles authentication, +headers, retries, and base URI handling for the Box API. + +.PARAMETER RelativeURI +Relative URI of the Box endpoint. +Example: "folders/123456" + +.PARAMETER Method +HTTP method (GET, POST, PUT, DELETE) + +.PARAMETER Body +Hashtable body that will be converted to JSON. + +.PARAMETER Upload +Switch indicating the upload API endpoint should be used. + +.EXAMPLE +Invoke-BoxRestCall -RelativeURI "folders/123" -Method GET + +.EXAMPLE +Invoke-BoxRestCall -RelativeURI "folders" -Method POST -Body $Body +#> + +function Invoke-BoxRestCall { + + [CmdletBinding(DefaultParameterSetName='Body')] + param ( + [Parameter(Mandatory)] + [string]$RelativeURI, + + [Parameter(Mandatory)] + [string]$Method, + + [hashtable]$Body, + + [switch]$Upload + ) + + begin { + + if ($null -eq $Script:BoxSession) { + throw "No Box session established. Run New-BoxSession first." + } + + if ($RelativeURI.StartsWith('/')) { + $RelativeURI = $RelativeURI.Substring(1) + } + + if ($Upload) { + $BaseURI = "https://upload.box.com/api/2.0/" + } + else { + $BaseURI = "https://api.box.com/2.0/" + } + } + + process { + + $IVRSplat = @{ + Headers = @{ + Authorization = "Bearer $($Script:BoxSession.AccessToken)" + "Content-Type" = "application/json" + } + Method = $Method + Uri = "$BaseURI$RelativeURI" + } + + if ($Body) { + $IVRSplat.Add("Body", ($Body | ConvertTo-Json -Depth 10)) + } + + Write-Verbose "Calling Box API: $Method $RelativeURI" + + try { + + $Result = Invoke-RestMethod @IVRSplat + $Script:APICallCount++ + + return $Result + } + catch { + + Write-Verbose "Box API call failed, retrying in 3 seconds..." + Start-Sleep -Seconds 3 + + $Result = Invoke-RestMethod @IVRSplat + $Script:APICallCount++ + + return $Result + } + } +} diff --git a/src/UofIBox/functions/public/New-BoxCollaboration.ps1 b/src/UofIBox/functions/public/New-BoxCollaboration.ps1 new file mode 100644 index 0000000..fa8e322 --- /dev/null +++ b/src/UofIBox/functions/public/New-BoxCollaboration.ps1 @@ -0,0 +1,72 @@ +<# +.SYNOPSIS +Adds collaborators to a Box folder. + +.DESCRIPTION +Assigns one or more users to a folder with specified roles. + +.PARAMETER FolderId +ID of the folder. + +.PARAMETER Login +User email(s). + +.PARAMETER Role +Role(s) assigned to users. + +.EXAMPLE +New-BoxCollaboration -FolderId 123456 -Login user@company.com -Role editor + +.EXAMPLE +New-BoxCollaboration ` + -FolderId 123456 ` + -Login user1@company.com,user2@company.com ` + -Role editor,viewer +#> + +function New-BoxCollaboration { + + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string]$FolderId, + + [Parameter(Mandatory)] + [string[]]$Login, + + [Parameter(Mandatory)] + [string[]]$Role + ) + + if ($Login.Count -ne $Role.Count) { + throw "Login and Role counts must match." + } + + for ($i = 0; $i -lt $Login.Count; $i++) { + + $Target = "$($Login[$i]) as $($Role[$i]) on folder $FolderId" + + if ($PSCmdlet.ShouldProcess("Box", "Add collaborator $Target")) { + + $Body = @{ + item = @{ + type = "folder" + id = $FolderId + } + accessible_by = @{ + type = "user" + login = $Login[$i] + } + role = $Role[$i] + } + + $Call = @{ + RelativeURI = "collaborations" + Method = "POST" + Body = $Body + } + + Invoke-BoxRestCall @Call + } + } +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/New-BoxFolder.ps1 b/src/UofIBox/functions/public/New-BoxFolder.ps1 new file mode 100644 index 0000000..10182e2 --- /dev/null +++ b/src/UofIBox/functions/public/New-BoxFolder.ps1 @@ -0,0 +1,45 @@ +<# +.SYNOPSIS +Creates a new folder in Box. + +.DESCRIPTION +Creates a folder in the specified parent folder. + +.PARAMETER FolderName +Name of the folder to create. + +.PARAMETER ParentFolderId +Parent folder ID. Defaults to root (0). + +.EXAMPLE +New-BoxFolder -FolderName "Finance" -ParentFolderId 0 +#> + +function New-BoxFolder { + + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string]$FolderName, + + [string]$ParentFolderId = "0" + ) + + if ($PSCmdlet.ShouldProcess("Box", "Create folder '$FolderName'")) { + + $Body = @{ + name = $FolderName + parent = @{ + id = $ParentFolderId + } + } + + $Call = @{ + RelativeURI = "folders" + Method = "POST" + Body = $Body + } + + Invoke-BoxRestCall @Call + } +} \ No newline at end of file diff --git a/src/UofIBox/functions/public/New-BoxSession.ps1 b/src/UofIBox/functions/public/New-BoxSession.ps1 new file mode 100644 index 0000000..b68a748 --- /dev/null +++ b/src/UofIBox/functions/public/New-BoxSession.ps1 @@ -0,0 +1,66 @@ +<# +.SYNOPSIS +Creates a Box API session. + +.DESCRIPTION +Authenticates to the Box API using client credentials and stores +the access token for use with other functions in the module. + +.PARAMETER Credential +Credential containing the Box Client ID and Client Secret. + +.PARAMETER EnterpriseId +Box enterprise ID. + +.EXAMPLE +$Credential = Get-Credential +New-BoxSession -Credential $Credential -EnterpriseId "123456" +#> + +function New-BoxSession { + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + "PSUseShouldProcessForStateChangingFunctions", + "")] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [PSCredential]$Credential, + + [Parameter(Mandatory)] + [string]$EnterpriseId + ) + + $Body = @{ + grant_type = "client_credentials" + client_id = $Credential.UserName + client_secret = $Credential.GetNetworkCredential().Password + box_subject_type = "enterprise" + box_subject_id = $EnterpriseId + } + + $TokenRequest = @{ + Method = "POST" + Uri = "https://api.box.com/oauth2/token" + Body = $Body + ContentType = "application/x-www-form-urlencoded" + } + + Write-Verbose "Authenticating to Box API" + + $Response = Invoke-RestMethod @TokenRequest + + if ($Response.access_token) { + + $Script:BoxSession = [PSCustomObject]@{ + AccessToken = $Response.access_token + Created = Get-Date + ExpiresIn = $Response.expires_in + } + + Write-Verbose "Box session created." + } + else { + throw "$_" + } +} diff --git a/src/UofIBox/functions/public/Receive-BoxFile.ps1 b/src/UofIBox/functions/public/Receive-BoxFile.ps1 new file mode 100644 index 0000000..628e828 --- /dev/null +++ b/src/UofIBox/functions/public/Receive-BoxFile.ps1 @@ -0,0 +1,36 @@ +<# +.SYNOPSIS +Downloads a file from Box. + +.DESCRIPTION +Downloads the specified Box file to a local path. + +.PARAMETER FileId +The ID of the Box file. + +.PARAMETER OutputPath +Local file path where the file will be saved. + +.EXAMPLE +Receive-BoxFile -FileId "123456" -OutputPath "C:\Downloads\file.pdf" +#> + +function Receive-BoxFile { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FileId, + + [Parameter(Mandatory)] + [string]$OutputPath + ) + + $DownloadCall = @{ + Method = "GET" + RelativeURI = "files/$FileId/content" + OutFile = $OutputPath + } + + Invoke-BoxRestCall @DownloadCall +} diff --git a/src/UofIBox/functions/public/Receive-BoxFolder.ps1 b/src/UofIBox/functions/public/Receive-BoxFolder.ps1 new file mode 100644 index 0000000..abf889e --- /dev/null +++ b/src/UofIBox/functions/public/Receive-BoxFolder.ps1 @@ -0,0 +1,75 @@ +<# +.SYNOPSIS +Downloads an entire Box folder. + +.DESCRIPTION +Recursively downloads all files and subfolders from a Box folder. + +.PARAMETER FolderId +The ID of the Box folder to download. + +.PARAMETER OutputDirectory +Local directory where files will be downloaded. + +.EXAMPLE +Receive-BoxFolder -FolderId "123456" -OutputDirectory "C:\BoxDownloads" +#> + +function Receive-BoxFolder { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FolderId, + + [Parameter(Mandatory)] + [string]$OutputDirectory + ) + + # Get folder metadata + $FolderCall = @{ + RelativeURI = "folders/$FolderId" + Method = "GET" + } + + $Folder = Invoke-BoxRestCall @FolderCall + + $LocalFolder = Join-Path $OutputDirectory $Folder.name + + if (-not (Test-Path $LocalFolder)) { + New-Item -ItemType Directory -Path $LocalFolder | Out-Null + } + + # Get folder items + $ItemsCall = @{ + RelativeURI = "folders/$FolderId/items" + Method = "GET" + } + + $Items = Invoke-BoxRestCall @ItemsCall + + foreach ($Item in $Items.entries) { + + if ($Item.type -eq "file") { + + $DownloadPath = Join-Path $LocalFolder $Item.name + + $DownloadCall = @{ + FileId = $Item.id + OutputPath = $DownloadPath + } + + Receive-BoxFile @DownloadCall + } + + if ($Item.type -eq "folder") { + + $SubFolderCall = @{ + FolderId = $Item.id + OutputDirectory = $LocalFolder + } + + Receive-BoxFolder @SubFolderCall + } + } +} diff --git a/src/UofIBox/functions/public/Remove-BoxFile.ps1 b/src/UofIBox/functions/public/Remove-BoxFile.ps1 new file mode 100644 index 0000000..5ecad1c --- /dev/null +++ b/src/UofIBox/functions/public/Remove-BoxFile.ps1 @@ -0,0 +1,30 @@ +<# +.SYNOPSIS +Deletes a Box file. + +.DESCRIPTION +Removes a file from Box permanently. + +.PARAMETER FileId +The ID of the file to delete. + +.EXAMPLE +Remove-BoxFile -FileId "123456789" +#> + +function Remove-BoxFile { + + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string]$FileId + ) + + if ($PSCmdlet.ShouldProcess("File $FileId", "Delete")) { + $Call = @{ + RelativeURI = "files/$FileId" + Method = "DELETE" + } + Invoke-BoxRestCall @Call + } +} diff --git a/src/UofIBox/functions/public/Remove-BoxFolder.ps1 b/src/UofIBox/functions/public/Remove-BoxFolder.ps1 new file mode 100644 index 0000000..0b55d1c --- /dev/null +++ b/src/UofIBox/functions/public/Remove-BoxFolder.ps1 @@ -0,0 +1,43 @@ +<# +.SYNOPSIS +Deletes a Box folder. + +.DESCRIPTION +Deletes the specified folder and its contents from Box. + +.PARAMETER FolderId +ID of the folder to delete. + +.PARAMETER Recursive +If set, deletes the folder and all of its contents. If not set, the folder must be empty to be deleted. + +.EXAMPLE +Remove-BoxFolder -FolderId "123456789" +#> + +function Remove-BoxFolder { + + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)] + [string]$FolderId, + + [switch]$Recursive + ) + + $Uri = "folders/$FolderId" + + if ($Recursive) { + $Uri += "?recursive=true" + } + + if ($PSCmdlet.ShouldProcess("Folder $FolderId", "Delete")) { + + $Call = @{ + RelativeURI = $Uri + Method = "DELETE" + } + + Invoke-BoxRestCall @Call + } +} diff --git a/src/UofIBox/functions/public/Send-BoxFile.ps1 b/src/UofIBox/functions/public/Send-BoxFile.ps1 new file mode 100644 index 0000000..1342244 --- /dev/null +++ b/src/UofIBox/functions/public/Send-BoxFile.ps1 @@ -0,0 +1,54 @@ +<# +.SYNOPSIS +Uploads a file to Box. + +.DESCRIPTION +Uploads a local file to a specified Box folder. + +.PARAMETER FilePath +Local path of the file to upload. + +.PARAMETER ParentFolderId +Box folder ID where the file will be uploaded. + +.EXAMPLE +Send-BoxFile -FilePath "C:\Temp\report.pdf" -ParentFolderId "123456" +#> + +function Send-BoxFile { + + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$FilePath, + + [Parameter(Mandatory)] + [string]$ParentFolderId + ) + + if ($null -eq $Script:BoxSession) { + throw "No Box session established. Run New-BoxSession first." + } + + $Attributes = @{ + name = [System.IO.Path]::GetFileName($FilePath) + parent = @{ + id = $ParentFolderId + } + } | ConvertTo-Json -Compress + + $Headers = @{ + Authorization = "Bearer $($Script:BoxSession.AccessToken)" + } + + $Form = @{ + attributes = $Attributes + file = Get-Item $FilePath + } + + Invoke-RestMethod ` + -Method POST ` + -Uri "$($Script:Settings.UploadBaseURI)files/content" ` + -Headers $Headers ` + -Form $Form +} diff --git a/test/UofIBox.Tests.ps1 b/test/UofIBox.Tests.ps1 new file mode 100644 index 0000000..f821848 --- /dev/null +++ b/test/UofIBox.Tests.ps1 @@ -0,0 +1,9 @@ +[String]$ModuleRoot = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'src\UofIBox' +Import-Module -Name $ModuleRoot + +Describe 'Module Manifest Tests' { + It 'Passes Test-ModuleManifest' { + $ManifestPath = Join-Path -Path "$(Split-Path -Path $PSScriptRoot -Parent)" -ChildPath 'src\UofIBox\UofIBox.psd1' + Test-ModuleManifest -Path $ManifestPath | Should -Not -BeNullOrEmpty + } +}