Table of Contents
Last updated: 2024-11-14

File API


Caution

Sample code provided in here is for demonstration and educational purposes only which provides customers with programming information regarding the products and is not intended for use in a production environment. All production code should always follow development best practises. All sample code here is supplied "AS IS" without any warranties or support.

This document explains how to use the "File API" to upload/download files to/from the "unmanaged files" in ShareAspace as well as how to delete files.

The provided C# code samples below uses HttpClient to make the REST calls.

Warning

A common mistake when using HttpClient is to use multiple instances of the HttpClient. You should reuse the same instance within an application.

Unmanaged file areas


There are several unmanaged file areas that you may freely add files and directories to. They are listed below and the URLs to access these areas via REST API.

Area URL
Collection /api/collection/fs/file
Space /api/space/[spaceId]/fs/file
User /api/space/[spaceId]/user/fs/file
Note: The actual user is not in the URL but is who you are authenticated as
Participant/[Id] /api/space/[spaceId]/participant/[participantId]/fs/file
Where 'participantId' is a valid participant in ShareAspace

The URLs above can in some cases have additional fields appended depending on the type of transfer you are doing and your file-path and file-name.

Authentication


To do any file API operations you must always first get an authentication token to be used in subsequent file API calls.

Below is a re-useable function for authentication that is used in all the following code sections.

CSharp

This code is used in all the other examples of code in this page. First there is an implementation of a BasicAuthenticationHeaderValue that is then used in the actual Authentication.

BasicAuthenticationHeaderValue

using System;
using System.Net.Http.Headers;
using System.Text;

namespace Demo.FileApi
{
    public class BasicAuthenticationHeaderValue : AuthenticationHeaderValue
    {
        public BasicAuthenticationHeaderValue(string clientId, string clientSecret)
            : base("Basic", EncodeCredential(clientId, clientSecret))
        {
        }

        private static string EncodeCredential(string clientId, string clientSecret)
        {
            return Convert.ToBase64String(
                Encoding.UTF8.GetBytes(string.Format("{0}:{1}", new object[2]
                {
                    clientId,
                    clientSecret
                })));
        }
    }
}

Authentication

using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Net.Http;

namespace Demo.FileApi
{
    public static partial class Program
    {
        public static JObject Authenticate(string baseAddress, string userName, string password, string clientId, string clientSecret)
        {
            HttpClient client = new HttpClient();
            client.DefaultRequestHeaders.Authorization = new BasicAuthenticationHeaderValue(clientId, clientSecret);

            var form = new Dictionary<string, string>
            {
                {"grant_type", "password"},
                {"username", userName},
                {"password", password},
                {"scope", "All" }
            };

            HttpContent content = new FormUrlEncodedContent(form);
            string requestUri = $"{baseAddress}/AuthorizationServer/sasweb/oauth/token";
            HttpResponseMessage response = client.PostAsync(requestUri, content).Result;
            response.EnsureSuccessStatusCode();
            string responseContent = response.Content.ReadAsStringAsync().Result;
            JObject token = JObject.Parse(responseContent);
            client.Dispose();
            return token;
        }
    }
}

Usage would look something like the following:

public static void TestAuthenticate()
{
    string baseAddress = @"https://my.machine1.net";
    string userName = "MyEmail";
    string password = "MyPassword";
    string clientName = "ResourceOwnerClientId";
    string clientSecret = "CLIENTSECRET";
    JObject token = Authenticate(baseAddress, userName, password, clientName, clientSecret);
}

PowerShell

Download get-authentication.ps1

function GetAuthentication($sasHost)
{
    #OAuth 2.0 settings
    $username = "MyEmail"
    $password = "MyPassword"
    $scope = "All"
    $clientId = "ResourceOwnerClientId"
    $clientSecret = "CLIENTSECRET"
    $grantType = "password"
    $authUri = -join ("https://", $sasHost, "/AuthorizationServer/sasweb/oauth/token")

    #Get bearerToken
    $basic = @{ 
        "Authorization" = ("Basic", [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(($clientId, $clientSecret -join ":"))) -join " ")
    }

    $authBody = @{ 
        "grant_type" = $grantType; 
        "username" = $username; 
        "password" = $password; 
        "scope" = $scope 
    }

    try{
        $response = Invoke-RestMethod -Method Post -Uri $authUri -ContentType "application/x-www-form-urlencoded" -Headers $basic -Body $authBody
        $global:bearerToken = $response.access_token
    }
    catch{
        $statusCode = $_.Exception.Response.StatusCode.value__
        $statusDescription = $_.Exception.Response.StatusDescription
        Write-Host "$($statusDescription)($($statusCode)); Uri: $($authUri); Response:$($_.Exception)"
        exit
    }

    $global:bearer = @{ "Authorization" = ("Bearer", $global:bearerToken -join " ") }
}

GetAuthentication "my.machine1.net"

This function puts into the PowerShell global variable space the authentication token in 2 formats. variable | usage ------|----- $global:bearerToken|Just the token in base64 format $global:bearer | The token in an object that looks like @{ "Authorization" = ("Bearer", $global:bearerToken -join " ") }. This is used only in the method Invoke-RestMethod

Upload files


When uploading single files to ShareAspace a checksum is mandatory. The logic for providing checksum is demonstrated further down. The checksum is a calculated hash based on the file to be uploaded, the checksum is added to the request as a header. The uploaded file will be validated in ShareAspace using the provided checksum.

Upload Stream

The upload stream method is at times too slow and takes too much memory but is very effective for files under 100 kB. It is also relatively easy to debug due to small amount of code which makes it very advantageous.

This stream technique requires that your URL from the section Unmanaged file areas has "/stream" appended to it as well as the destination subdirectories and file-name. If you want to put the file in the root then don't include any subdirectory. For example in the collection area with subdirectory MySubDir and file MyFile.txt.

Area URL
collection /api/collection/fs/file/stream/MySubDir/MyFile.txt

An example of the HTTPS call is the following:

POST https://essevm540.es.eurostep.com/api/space/InReachAdmin/fs/file/stream/testImport.xlsx
Content-Type: application/data
Authorization: Bearer eyJ0eXAi...
Content-Length: 9440

Below is a method that will upload a file via the stream method.

CSharp

For this to work you need to include the function in the code from the section Authentication.

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;

namespace Demo.FileApi
{
    public static partial class Program
    {
        public static void UploadUserStoreFile(string accessToken, string baseAddress, string targetFilePath, string sourceFilePath, string spaceId)
        {
            using (var file = new FileStream(sourceFilePath, FileMode.Open))
            {
                string requestUri = $"{baseAddress}/api/space/{spaceId}/user/fs/file/stream/{targetFilePath}";
                HttpClient client = new HttpClient();
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                string contentType = MimeMapping.GetMimeMapping(sourceFilePath);

                int size = (int)file.Length;
                byte[] buffer = new byte[size];
                file.Read(buffer, 0, size);
                var hasher = new MD5CryptoServiceProvider();
                byte[] hash = hasher.ComputeHash(buffer);
                string checksum = BitConverter.ToString(hash).Replace("-", "").ToLower();
                client.DefaultRequestHeaders.Add("checksum", checksum);

                file.Position = 0;
                var content = new StreamContent(file);
                content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
                HttpResponseMessage response = client.PostAsync(requestUri, content).Result;
                response.EnsureSuccessStatusCode();
                client.Dispose();
            }
        }
    }
}

Usage would look something like the following:

public static void TestUploadFileStream()
{
    string baseAddress = @"https://my.machine1.net";
    string userName = "MyEmail";
    string password = "MyPassword";
    string clientName = "ResourceOwnerClientId";
    string clientSecret = "CLIENTSECRET";
    string spaceId = "mytestspace";
    string remoteFilePath = @"MyFolder/TestFile.txt";
    string localFilePath = @"..\..\..\..\files\TestFile.txt";
    JObject token = Authenticate(baseAddress, userName, password, clientName, clientSecret);
    string accessToken = (string)token["access_token"];
    UploadUserStoreFile(accessToken, baseAddress, remoteFilePath, localFilePath, spaceId);
}

PowerShell

Download upload-file-stream.ps1

function UploadFileStream($sasHost, $destFileUrlPath, $srcFilePath, $bearerToken)
{
  <#
    .SYNOPSIS
        Upload moderately sized files ( less then 1.3 MB ) to ShareASpace Nova
    .DESCRIPTION
        This will fail if the file already exists on the server.
        When uploading files 1 MB or larger this will eat up ALL memory and be very slow.
    .EXAMPLE
        Load a file to the Collection files in the root
        UploadFileStream "essevm540.es.eurostep.com" "/api/collection/fs/file/stream/smallfile.json" "C:\Users\sassetup\Desktop\smallfile.json" $global:bearerToken
    .EXAMPLE
        Load a file to the Collection files in a sub-directory called AutoImport. Non-existing subdirectories created automatically
        UploadFileStream "essevm540.es.eurostep.com" "/api/collection/fs/file/stream/AutoImport/smallfile.json" "C:\Users\sassetup\Desktop\smallfile.json" $global:bearerToken
    .EXAMPLE
        Load a file to the User files in a sub-directory called AutoImport
        UploadFileStream "essevm540.es.eurostep.com" "/api/space/MySpaceName/user/fs/file/stream/AutoImport/smallfile.json" "C:\Users\sassetup\Desktop\smallfile.json" $global:bearerToken
    .EXAMPLE
        Load a file to the space files in a sub-directory called AutoImport
        UploadFileStream "essevm540.es.eurostep.com" "/api/space/MySpaceName/fs/file/stream/AutoImport/smallfile.json" "C:\Users\sassetup\Desktop\smallfile.json" $global:bearerToken
    .PARAMETER sasHost
        The FQDN of your host server.
    .PARAMETER destFileUrlPath
        A url that contains the information on where in the unmanaged file system you want to put the file. It may include the Space name. Non-existing subdirectories created automatically
        It looks like either
        /api/space/[SpaceName]/[user or participant or 'nothing for space' or participant/OEM]/fs/file/stream/[subdirectory(one or more)]/[filename]
        or
        /api/collection/fs/file/stream/[subdirectory(ies)]/[filename]
    .PARAMETER srcFilePath
        Where in your local machine the file to upload exists. Full path and file name
    .PARAMETER bearerToken
        The authentication token received from a request for autentication.
  #>
    try{
        $chunk = [System.IO.File]::ReadAllBytes($srcFilePath)
        $hasher = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider
        $hash = $hasher.ComputeHash($chunk)
        $checksum = [System.BitConverter]::ToString($hash).Replace("-","").ToLower()
        $headers = @{"Authorization"="Bearer $bearerToken";"checksum"=$checksum;}
        $requestUri = "https://{0}{1}" -f $sasHost, $destFileUrlPath
        $response = Invoke-RestMethod -Uri $requestUri -Method Post -InFile $srcFilePath -Headers $headers
    }
    catch{
        $statusCode = $_.Exception.Response.StatusCode.value__
        $statusDescription = $_.Exception.Response.StatusDescription
        Write-Host "$($statusDescription)($($statusCode)); Uri: $($requestUri); Response: $($_.Exception)"
    }
}

UploadFileStream "my.machine1.net" "/api/space/mytestspace/user/fs/file/stream/UploadFolder/TestFile.txt" "C:\temp\TestFile.txt" $global:bearerToken

Upload Chunk

The "Chunk" technique for uploading is the only method recommended for large files over 1.5 MB. This technique works for small files. However, it is quite slow for small files since it allocates a relatively large buffer of memory for the operation. The code is also much larger and more complex and difficult to debug.

With this "chunk" technique the URLs from the section Unmanaged file areas are unchanged. The paths for subdirectories and the file-name are included in the body of the message instead of in the URL as the "Stream" method employs.

This technique has the downside that it uses multiple HTTPS calls to ShareAspace and if this technique crashes in the middle of the execution then you will probably have a partial file in ShareAspace and there is no way of knowing that it is a partial file. You may therefore want to change the code below to delete the file from ShareAspace in the case of a failure during uploading.

An example of the HTTPS call is the following:

POST https://essevm540.es.eurostep.com/api/space/InReachAdmin/user/fs/file 
Authorization: Bearer eyJ0eXAiOi...
Content-Type: multipart/form-data; boundary="49e051f4-40e2-4ea6-a982-2fd0efaf8fc7"
Host: essevm540.es.eurostep.com
Content-Length: 1293315

A re-useable function to upload files using the chunk method.

CSharp

For this to work you need to include the function in the code from the section Authentication.

using Microsoft.AspNetCore.StaticFiles;
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;

namespace Demo.FileApi
{
    public static partial class Program
    {
        public static void UploadUserStoreFileChunked(string accessToken, string baseAddress, string targetFilePath, string sourceFilePath, string spaceId)
        {
            using (FileStream file = new FileStream(sourceFilePath, FileMode.Open))
            {
                var requestUri = $"{baseAddress}/api/space/{spaceId}/user/fs/file";
                string fileName = Path.GetFileName(sourceFilePath);
                HttpClient client = new HttpClient();
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

                var fileExtContentTypeProvider = new FileExtensionContentTypeProvider();
                                
                if(!fileExtContentTypeProvider.TryGetContentType(fileName, out string contentType))
                {
                    contentType = "application/octet-stream";
                }

                int chunkSize = 4 * 1024 * 1024;
                long totalSize = file.Length;
                int chunkNumber = 0;

                int totalNumberOfChunks = (int)Math.Truncate((double)file.Length / (double)chunkSize);
                if (Convert.ToInt32(file.Length % chunkSize) != 0)
                {
                    totalNumberOfChunks += 1;
                }

                // dummy checksum to be deleted later
                client.DefaultRequestHeaders.Add("checksum", "checksum");

                var hasher = new MD5CryptoServiceProvider();

                // initial call
                while (true)
                {
                    var buffer = new byte[chunkSize];
                    int read = file.Read(buffer, 0, chunkSize);
                    byte[] chunk;
                    int currentChunkSize;
                    if (read == 0)
                    {
                        break;
                    }
                    else if (read < chunkSize)
                    {
                        // this will be for the last chunk when we cant fill the buffer
                        chunk = new byte[read];
                        Array.Copy(buffer, chunk, read);
                        currentChunkSize = read;
                    }
                    else
                    {
                        chunk = buffer;
                        currentChunkSize = chunkSize;
                    }

                    byte[] hash = hasher.ComputeHash(chunk);
                    string checksum = BitConverter.ToString(hash).Replace("-", "").ToLower();
                    client.DefaultRequestHeaders.Remove("checksum");
                    client.DefaultRequestHeaders.Add("checksum", checksum);

                    // upload chunk
                    using (var bufferStream = new MemoryStream(chunk))
                    {
                        using (MultipartFormDataContent content = new MultipartFormDataContent())
                        {
                            bufferStream.Position = 0;
                            using (var stream = new StreamContent(bufferStream))
                            {
                                content.Add(stream, "file", fileName);

                                var resumableTotalSize = new StringContent(totalSize.ToString());
                                content.Add(resumableTotalSize, "resumableTotalSize");

                                var resumableChunkNumber = new StringContent(((int)chunkNumber + 1).ToString());
                                content.Add(resumableChunkNumber, "resumableChunkNumber");

                                var resumableChunkSize = new StringContent(chunkSize.ToString());
                                content.Add(resumableChunkSize, "resumableChunkSize");

                                var resumableCurrentChunkSize = new StringContent(currentChunkSize.ToString());
                                content.Add(resumableCurrentChunkSize, "resumableCurrentChunkSize");

                                var resumableTotalChunks = new StringContent(totalNumberOfChunks.ToString());
                                content.Add(resumableTotalChunks, "resumableTotalChunks");

                                var resumableType = new StringContent(contentType);
                                content.Add(resumableType, "resumableType");

                                var resumableFileName = new StringContent(fileName);
                                content.Add(resumableFileName, "resumableFilename");                                

                                var resumableRelativePath = new StringContent(targetFilePath);
                                content.Add(resumableRelativePath, "resumableRelativePath");

                                using (HttpResponseMessage response = client.PostAsync(requestUri, content).Result)
                                {
                                    response.EnsureSuccessStatusCode();
                                }
                            }
                        }
                    }
                    chunkNumber += 1;
                }
                hasher.Dispose();
                client.Dispose();
            }
        }
    }
}

Usage would look something like the following:

public static void TestUploadFileChunked()
{
    string baseAddress = @"https://my.machine1.net";
    string userName = "MyEmail";
    string password = "MyPassword";
    string clientName = "ResourceOwnerClientId";
    string clientSecret = "CLIENTSECRET";
    string spaceId = "mytestspace";
    string remoteFilePath = @"MyFolder/BigFile.pdf";
    string localFilePath = @"..\..\..\..\files\BigFile.pdf";
    JObject token = Authenticate(baseAddress, userName, password, clientName, clientSecret);
    string accessToken = (string)token["access_token"];
    UploadUserStoreFileChunked(accessToken, baseAddress, remoteFilePath, localFilePath, spaceId);
}

PowerShell

Download upload-file-chunked.ps1

function UploadFileChunked($sasHost, $destUrl, $destFileFullName, $srcFileFullName, $fileName, $bearerToken)
{
  <#
    .SYNOPSIS
        Upload small to HUGE files to ShareASpace Nova using the Chunk method
    .DESCRIPTION
        This will fail if the file already exists on the server.
        There is a path to the file System.Net.Http.dll inside this function that you may need to set for your environment
    .EXAMPLE
        Load a file to the Collection files in the root
        UploadFileChunked "essevm540.es.eurostep.com" "/api/collection/fs/file" "BigFile.json" "C:\Users\sassetup\Desktop\BigFile.json" "BigFile.json" $global:bearerToken
    .EXAMPLE
        Load a file to the Collection files in a sub-directory called AutoImport. Non-existing subdirectories created automatically
        UploadFileChunked "essevm540.es.eurostep.com" "/api/collection/fs/file" "AutoImport/BigFile.json" "C:\Users\sassetup\Desktop\BigFile.json" "BigFile.json" $global:bearerToken
    .EXAMPLE
        Load a file to the User files in a sub-directory called AutoImport
        UploadFileChunked "essevm540.es.eurostep.com" "/api/space/MySpaceName/user/fs/file" "AutoImport/BigFile.json" "C:\Users\sassetup\Desktop\BigFile.json" "BigFile.json" $global:bearerToken 
    .EXAMPLE
        Load a file to the space files in a sub-directory called AutoImport
        UploadFileChunked "essevm540.es.eurostep.com" "/api/space/MySpaceName/fs/file" "AutoImport/BigFile.json" "C:\Users\sassetup\Desktop\BigFile.json" "BigFile.json" $global:bearerToken 
    .PARAMETER sasHost
        The FQDN of your host server.
    .PARAMETER destUrl
        A url that contains the information on where in the unmanaged file system you want to put the file. It may include the Space name.
        It looks like either
        /api/space/[SpaceName]/[user or participant or 'nothing for space' or participant/OEM]/fs/file
        or
        /api/collection/fs/file
    .PARAMETER destFileFullName
        This is the destination name of your file with any relative path. If just a filename the file is put on the root. Non-existing subdirectories created automatically. It looks like
        [directoryA]/[directoryB]/[filename.ext]
    .PARAMETER srcFileFullName
        Where in your local machine the file to upload exists. Full path and file name
    .PARAMETER fileName
        Just the filename. This is used to get the MIME information
    .PARAMETER bearerToken
        The authentication token received from a request for autentication.
  #>

    try{
        #You must put here the correct path here to your System.Net.Http.dll . Sometimes you may also comment out this line
        Add-Type -Path 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\System.Net.Http.dll'

        $md5 = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider
        $httpClientHandler = New-Object System.Net.Http.HttpClientHandler
        $httpClient = New-Object System.Net.Http.Httpclient $httpClientHandler

        $httpClient.DefaultRequestHeaders.Add("Authorization", -join("Bearer", " ", $bearerToken))

        $packageFileStream = New-Object System.IO.FileStream @($srcFileFullName, [System.IO.FileMode]::Open)

        $contentDispositionHeaderValue = New-Object System.Net.Http.Headers.ContentDispositionHeaderValue "form-data"
        $contentDispositionHeaderValue.Name = "fileData"

        $contentDispositionHeaderValue.FileName = (Split-Path $fileName -leaf)

        $contentType = ""

        $mimeType = [System.Web.MimeMapping]::GetMimeMapping($fileName)

        if ($mimeType)
        {
            $contentType = $mimeType
        }
        else
        {
            $contentType = "application/octet-stream"
        }

        $streamContent = New-Object System.Net.Http.StreamContent $packageFileStream
        $streamContent.Headers.ContentDisposition = $contentDispositionHeaderValue

        $streamContent.Headers.ContentType = New-Object System.Net.Http.Headers.MediaTypeHeaderValue $contentType

        $chunkSize = 4 * 1024 * 1024
        $totalSize = $packageFileStream.Length
        $chunkNumber = 0

        $totalNumberOfChunks = [System.Convert]::ToInt32([System.Math]::Truncate($packageFileStream.Length / $chunkSize))

        if ([System.Convert]::ToInt32($packageFileStream.Length % $chunkSize) -ne 0)
        {
             $totalNumberOfChunks += 1;
        }

        $httpClient.DefaultRequestHeaders.Add("checksum","123")

        # initial call
        while($true)
        {
            $buffer = New-Object byte[] $chunkSize

            [System.IO.MemoryStream]$ss = New-Object -TypeName System.IO.MemoryStream -ArgumentList (,$buffer)

            $r = $packageFileStream.Read($buffer, 0, $chunkSize)

            if($r -eq 0)
            {
                break
            }

            $ssByteArr = $buffer
            $currentChunkSize = $chunkSize

            # this will be for the last chunk when we cant fill the buffer
            if($r -lt $chunkSize)
            {
                $buffer2 = New-Object byte[] $r
                [Array]::Copy($buffer, $buffer2, $r)

                $ss = New-Object -TypeName System.IO.MemoryStream -ArgumentList (,$buffer2)

                $ssByteArr =$buffer2
                $currentChunkSize = $r
            }

            $hash = [System.BitConverter]::ToString($md5.ComputeHash($ssByteArr)).Replace("-","").ToLower();

            $x = $httpClient.DefaultRequestHeaders.Remove("checksum")
            $httpClient.DefaultRequestHeaders.Add("checksum",$hash)
            $ss.Position = 0

            # upload chunk
            $content = New-Object System.Net.Http.MultipartFormDataContent
            $sc = New-Object System.Net.Http.StreamContent($ss)
            $content.Add($sc, "file", $fileName)

            $resumableTotalSize = New-Object System.Net.Http.StringContent($totalSize)

            $content.Add($resumableTotalSize, "resumableTotalSize")
            $resumableChunkNumber = New-Object System.Net.Http.StringContent([int]$chunkNumber + 1)
            $content.Add($resumableChunkNumber, "resumableChunkNumber")
            $resumableChunkSize = New-Object System.Net.Http.StringContent($chunkSize)
            $content.Add($resumableChunkSize, "resumableChunkSize")
            $resumableCurrentChunkSize = New-Object System.Net.Http.StringContent($currentChunkSize)
            $content.Add($resumableCurrentChunkSize, "resumableCurrentChunkSize")
            $resumableTotalChunks = New-Object System.Net.Http.StringContent($totalNumberOfChunks)
            $content.Add($resumableTotalChunks, "resumableTotalChunks")
            $resumableType = New-Object System.Net.Http.StringContent($mimeType)
            $content.Add($resumableType, "resumableType")
            $resumableFileName = New-Object System.Net.Http.StringContent($fileName)
            $content.Add($resumableFileName, "resumableFilename")
            $resumableRelativePath = New-Object System.Net.Http.StringContent($destFileFullName)
            $content.Add($resumableRelativePath, "resumableRelativePath")

            try
            {
                $requestUri = "https://{0}{1}" -f $sasHost, $destUrl
                $response = $httpClient.PostAsync($requestUri, $content).Result
                $statusCode = $response.StatusCode

                if (!$response.IsSuccessStatusCode)
                {
                    $responseBody = $response.Content.ReadAsStringAsync().Result
                    $errorMessage = "Status code {0}. Reason {1}. Server reported the following message: {2}." -f $response.StatusCode, $response.ReasonPhrase, $responseBody
                    throw [System.Net.Http.HttpRequestException] $errorMessage
                }

                $result = $response.Content.ReadAsStringAsync().Result
            }

            catch [Exception]
            {
                $PSCmdlet.ThrowTerminatingError($_)
            }

            finally
            {
                if($null -ne $response)
                {
                    $response.Dispose()
                }

                if($null -ne $content)
                {
                    $content.Dispose()
                }

                if($null -ne $sc)
                {
                    $sc.Dispose()
                }
            }

            $chunkNumber += 1
        }

        if($null -ne $packageFileStream)
        {
             $packageFileStream.Dispose()
        }
    }

    catch {
        $statusCode = $_.Exception.Response.StatusCode.value__
        $statusDescription = $_.Exception.Response.StatusDescription
        Write-Log -Level Error -Message "UploadFileChunked - $($statusDescription)($($statusCode)); Response:$($_.Exception)"
    }
}

UploadFileChunked "my.machine1.net" "/api/space/mytestspace/user/fs/file" "UploadFolder/TestFile.txt" "C:\temp\TestFile.txt" "TestFile.txt" $global:bearerToken 

Upload Bulk

There are two admin routes that enable direct upload of a folder structure with files through a HTTP Post to ShareAspace at the three levels:

  • collection level
  • space level
  • participant level

To accomplish this two admin routes are used:

  • admin/fileupload/
  • admin/fileupload/{spaceId}/{participantId?}

These admin routes needs the AdministrationApi key to work.

Request body params:

  • sourceDirectory (mandatory) - This is where your files are on disk, the ShareAspace host needs access to that folder
  • writeLog (mandatory) - Set this to true to get a detailed JSON log generated at sourceDirectory when upload is finished
  • targetDirectory (defaulted to "/") - This is the relative path in ShareAspace where the file structure should end up
  • replaceFiles (optional bool, defaulted to false) - If set, the files that already exist in ShareAspace will be replaced
  • deleteFilesAfterUpload (optional bool, defaulted to false) - If set, the files on disk will be deleted from the sourceDirectory, use with caution

If writeLog setting is set to true a JSON log is automatically created at the sourceDirectory root when upload is finished. FileUpload_timestamp.log. This will contain the settings used for upload, details about each file, the eventual errors and some file upload statistics.

CSharp

using Newtonsoft.Json.Linq;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;

namespace Demo.FileApi
{
    public static partial class Program
    {
        public static void BulkUploadFiles(string baseAddress, string sourceDirectory, string apiKey, bool writeLog, string spaceId = null, string participantId = null, string targetDirectory = "/", bool replaceFiles = false, bool deleteFilesAfterUpload = false)
        {
            string path;
            if (spaceId == null)
            {
                path = $"/admin/fileupload";
            }
            else if (spaceId != null && participantId != null)
            {
                path = $"/admin/fileupload/{spaceId}/{participantId}";
            }
            else
            {
                path = $"/admin/fileupload/{spaceId}";
            }

            JObject data = new JObject();
            data["sourceDirectory"] = sourceDirectory;
            data["targetDirectory"] = targetDirectory;
            data["replaceFiles"] = replaceFiles;
            data["deleteFilesAfterUpload"] = deleteFilesAfterUpload;
            data["writeLog"] = writeLog;

            //Hash the data
            byte[] apiKeys = Convert.FromBase64String(apiKey);
            HMACSHA512 hmacsha = new HMACSHA512(apiKeys);
            byte[] hashed = hmacsha.ComputeHash(Encoding.UTF8.GetBytes(path));

            // Convert to proper format
            string output = Convert.ToBase64String(hashed);
            output = output.Split('=')[0]; // Remove any trailing '='s
            output = output.Replace('+', '-'); // 62nd char of encoding
            output = output.Replace('/', '_'); // 63rd char of encoding

            HttpClient client = new HttpClient();
            client.BaseAddress = new Uri(baseAddress);
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", output);

            StringContent postParams = new StringContent(data.ToString(), Encoding.UTF8, "application/json");
            HttpResponseMessage response = client.PostAsync(path, postParams).Result;
            response.EnsureSuccessStatusCode();
            client.Dispose();
        }
    }
}

Usage would look something like the following:

public static void TestBulkUploadFiles()
{
    string baseAddress = @"http://localhost:5000";
    string sourceDirectory = @"C:\UploadFolder";
    string adminApiSymmetricKey = "8JcfLlO+usfLslIIUNIYofT8FI17l93je/QK6jE/Yhm+cKd/N2iaV26qElc9qpuLNP/ySy/2lAxeIVKd9ONt+w==";
    bool writeLog = true;
    string spaceId = "mytestspace";
    string participantId = "OEM";
    string targetDirectory = "/";
    bool replaceFiles = true;
    bool deleteFilesAfterUpload = false;
    BulkUploadFiles(baseAddress, sourceDirectory, adminApiSymmetricKey, writeLog, spaceId, participantId, targetDirectory, replaceFiles, deleteFilesAfterUpload);
}

PowerShell

Download bulk-upload-files.ps1

<#
.SYNOPSIS
    This PowerShell scripts uploads files from a local folder to unmanaged files for a collection, space or a paricipant

.DESCRIPTION
    This script takes 3 mandatory parameters. Before executing this script please make sure service host is running.  

.PARAMETER baseAddress 
    First parameter is baseAddress. It is a mandatory parameter. BaseAddress must be as http://localhost:5000 or http://my.machine1.net:5000.

.PARAMETER sourceDirectory
    Second parameter is sourceDirectory. It is a mandatory parameter. SourceDirectory is where the files are resided that should be uploaded. Must be an absolute path.

.PARAMETER apiKey 
    Third parameter is apiKey. It is  a mandatory parameter. Apikey can be found from bootstrap_collection.json

.PARAMETER writeLog
    Fourth parameter is writeLog. It is a mandatory boolean parameter. If set to true a json log will be generated at the sourceDirectory when upload finish.

.PARAMETER spaceId
    Fifth parameter is spaceId. It is an optional parameter. SpaceId is the ID of space. Files will be uploaded to collection if not specified.

.PARAMETER participantId
    Sixth parameter is participantId. It is an optional parameter. ParticipantId is the id of participant. This must be used in combination with spaceId. Files will be uploaded to the participant.

.PARAMETER targetDirectory
    Seventh parameter is targetDirectory. It is an optional parameter. targetDirectory is the relative folder path where the files should be uploaded. eg -targetDirectory "/targetFolder/" defaulted to "/"
    
.PARAMETER replaceFiles
    Eighth parameter is replaceFiles. It is an optional parameter defaulted to false. If set to true it will replace existing files in targetDirectory.

.PARAMETER deleteFilesAfterUpload
    Ninth parameter is deleteFilesAfterUpload. It is an optional parameter defaulted to false. DeleteFilesAfterUpload if set to true will remove the uploaded files from the sourceDirectory.

.EXAMPLE

C:\PS> .\bulk-upload-files.ps1
If no parameter provided it will ask to enter mandatory parameters. It will display both success and failure results     

.EXAMPLE

C:\PS> .\bulk-upload-files.ps1  -baseAddress http://localhost:5000 -sourceDirectory "C:\UploadFolder" -apiKey apikey -writeLog $true
All mandatory parameters provided. It will display both success and failure results     
#>

param (
    [Parameter(Position=1, Mandatory=$true)]
    [String]$baseAddress,

    [Parameter(Position=2, Mandatory=$true)]
    [String]$sourceDirectory,

    [Parameter(Position=3, Mandatory=$true)]
    [String]$apiKey,

    [Parameter(Position=4, Mandatory=$true)]
    [Bool]$writeLog,
    
    [Parameter(Position=5, Mandatory=$false)]
    [String]$spaceId = $null,
    
    [Parameter(Position=6, Mandatory=$false)]
    [String]$participantId = $null,
    
    [Parameter(Position=7, Mandatory=$false)]
    [String]$targetDirectory = "/",
    
    [Parameter(Position=8, Mandatory=$false)]
    [Bool]$replaceFiles = $false,
    
    [Parameter(Position=9, Mandatory=$false)]
    [Bool]$deleteFilesAfterUpload = $false
)

# Add the necessary .NET assembly
Add-Type -AssemblyName System.Net.Http

$apiKeys = [System.Convert]::FromBase64String($apiKey);
if (!$spaceId)
{
    $path = "/admin/fileupload";
}
elseif ($spaceId -and $participantId)
{
    $path = "/admin/fileupload/$spaceId/$participantId";
}
else
{
    $path = "/admin/fileupload/$spaceId"; 
}

# Setup json data
$data = @{
    sourceDirectory = "$sourceDirectory";
    targetDirectory = "$targetDirectory";
    replaceFiles = "$replaceFiles";
    deleteFilesAfterUpload = $deleteFilesAfterUpload;
    writeLog = $writeLog} | ConvertTo-Json

# Create the HttpClient client
$client = New-Object -TypeName System.Net.Http.Httpclient
# Set base address
$client.BaseAddress = $baseAddress;
# Hash the data
$hmacsha = New-Object System.Security.Cryptography.HMACSHA512;
$hmacsha.key = $apiKeys;
$hashed = $hmacsha.ComputeHash([system.Text.Encoding]::UTF8.GetBytes($path));    
# Convert to proper format
$output = [System.Convert]::ToBase64String($hashed);
$output = $output.Split('=')[0]; # Remove any trailing '='s
$output = $output.Replace('+', '-'); # 62nd char of encoding
$output = $output.Replace('/', '_'); # 63rd char of encoding
# Setup  HttpClient client for the secure call
$client.DefaultRequestHeaders.Authorization =  New-Object -TypeName System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", $output);
$encoding = New-Object -TypeName system.Text.UTF8Encoding;
$postParams = New-Object -TypeName System.Net.Http.StringContent($data, $encoding, "application/json");
# PostAsync 
$response = $client.PostAsync($path, $postParams).Result;
# check If it is failed 
if($response.IsSuccessStatusCode -eq $false)
{
    write-host $response.StatusCode -foregroundcolor "Red";
    Write-Host "Failed to upload files" -ForegroundColor "Red";
    return;
}
else
{
    Write-Host $response.RequestMessage.RequestUri -ForegroundColor Magenta -BackgroundColor White
    Write-Host "File upload request successful, check log file after completion for details" -ForegroundColor Magenta -BackgroundColor White
}

An example of usage:

.\bulk-upload-files.ps1  -baseAddress http://localhost:5000 -sourceDirectory "C:\UploadFolder" -apiKey 8JcfLlO+usfLslIIUNIYofT8FI17l93je/QK6jE/Yhm+cKd/N2iaV26qElc9qpuLNP/ySy/2lAxeIVKd9ONt+w== -writeLog $true

Download files


Downloading files is a very simple process that requires that your URL from the section Unmanaged file areas has appended to it the source subdirectories and file-name. If the file is in the root then don't include any subdirectory. For example in the collection area with subdirectory MySubDir and file MyFile.txt

Area URL
collection /api/collection/fs/file/MySubDir/MyFile.txt

An example of the HTTPS call is the following:

GET https://essevm540.es.eurostep.com/api/space/InReachAdmin/participant/OEM/fs/file/AutoImport/testImport.xlsx?token=eyJ0eXAiOiJKV1Qi...

Note that in the above call the token is in the URL. There are no headers. This is a very simple technique.

CSharp

For this to work you need to include the function in the code from the section Authentication.

using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;

namespace Demo.FileApi
{
    public static partial class Program
    {
        public static void DownloadUserStoreFile(string accessToken, string baseAddress, string sourceFilePath, string targetFilePath, string spaceId)
        {
            string requestUri = $"{baseAddress}/api/space/{spaceId}/user/fs/file/{sourceFilePath}";
            HttpClient client = new HttpClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            Stream response = client.GetStreamAsync(requestUri).Result;
            using (FileStream file = new FileStream(targetFilePath, FileMode.Create))
            {
                response.CopyTo(file);
            }
            client.Dispose();
        }
    }
}

Usage would look something like the following:

public static void TestDownloadFile()
{
    string baseAddress = @"https://my.machine1.net";
    string userName = "MyEmail";
    string password = "MyPassword";
    string clientName = "ResourceOwnerClientId";
    string clientSecret = "CLIENTSECRET";
    string spaceId = "mytestspace";
    string remoteFilePath = @"MyFolder/TestFile.txt";
    string localFilePath_user = @"..\..\..\..\files\TestFile_user.txt";
    JObject token = Authenticate(baseAddress, userName, password, clientName, clientSecret);
    string accessToken = (string)token["access_token"];
    DownloadUserStoreFile(accessToken, baseAddress, remoteFilePath, localFilePath_user, spaceId);
}

PowerShell

The code example below uses a System.Net.WebClient object to execute this HTTPS call. The reason this object is used is because of its speed and simplicity. However, there is no indication of downloading progress. If you want a function that shows the downloading progress then use the function "Invoke-WebRequest -Uri $url -OutFile $output" instead with the same URL as in the code below. The Invoke-WebRequest is many times slower than the technique shown below.

Download download-file.ps1

function DownloadFile($sasHost, $srcFileUrlPath, $localFullName, $bearerToken)
{
  <#
    .SYNOPSIS
        Download files from ShareASpace Nova
    .DESCRIPTION
        fails if the file does not exist on the server
    .EXAMPLE
        Download a file from the Collection files in the root
        DownloadFile "essevm540.es.eurostep.com" "/api/collection/fs/file/myfile.txt" "C:\Users\sassetup\Desktop\myfile.txt" $global:bearerToken
    .EXAMPLE
        Download a file from the Collection files in a sub-directory called AutoImport.
        DownloadFile "essevm540.es.eurostep.com" "/api/collection/fs/file/AutoImport/myfile.txt" "C:\Users\sassetup\Desktop\myfile.txt" $global:bearerToken
    .EXAMPLE
        Download a file from the User files in a sub-directory called AutoImport
        DownloadFile "essevm540.es.eurostep.com" "/api/space/MySpaceName/user/fs/file/AutoImport/myfile.txt" "C:\Users\sassetup\Desktop\myfile.txt" $global:bearerToken
    .EXAMPLE
        Download a file from the space files in a sub-directory called AutoImport
        DownloadFile "essevm540.es.eurostep.com" "/api/space/MySpaceName/fs/file/AutoImport/myfile.txt" "C:\Users\sassetup\Desktop\myfile.txt" $global:bearerToken
    .PARAMETER sasHost
        The FQDN of your host server.
    .PARAMETER srcFileUrlPath
        A url that contains the information on where in the unmanaged file system you want to remove the file. It may include the Space name. 
        It looks like either
        /api/space/[SpaceName]/[user or participant or 'nothing for space' or participant/OEM]/fs/file/[subdirectory(one or more)]/[filename]
        or
        /api/collection/fs/file/[subdirectory(one or more)]/[filename]
    .PARAMETER localFullName
        The full filename and path to where you want the file put on your local drive
    .PARAMETER bearerToken
        The authentication token received from a request for autentication.
  #>
    try{
        $headers = @{"Authorization"="Bearer $bearerToken";}
        $requestUri = "https://{0}{1}" -f $sasHost, $srcFileUrlPath
        $response = Invoke-RestMethod -Method Get -Uri $requestUri -Headers $headers -OutFile $localFullName
    }
    catch{			
        $statusCode = $_.Exception.Response.StatusCode.value__
        $statusDescription = $_.Exception.Response.StatusDescription
        Write-Host "$($statusDescription)($($statusCode)); Uri: $($url); Response: $($_.Exception)"
    } 
}

DownloadFile "my.machine1.net" "/api/space/mytestspace/user/fs/file/UploadFolder/TestFile.txt" "C:\temp\TestFile.txt" $global:bearerToken

Delete files


Deleting files is a very simple process that requires that your URL from the section Unmanaged file areas has appended to it the subdirectories and file-name of the file you want to delete. If the file is in the root then don't include any subdirectory. For example in the collection area with subdirectory MySubDir and file MyFile.txt

Area URL
collection /api/collection/fs/file/MySubDir/MyFile.txt

An example of the HTTPS call is the following:

DELETE https://essevm540.es.eurostep.com/api/space/InReachAdmin/participant/OEM/fs/file/AutoImport/testImport.xlsx 
Authorization: Bearer eyJ0eXAiOiJKV1Q...

CSharp

For this to work you need to include the function in the code from the section Authentication.

using System.Net.Http;
using System.Net.Http.Headers;

namespace Demo.FileApi
{
    public static partial class Program
    {
        public static void DeleteUserStoreFile(string accessToken, string baseAddress, string targetFilePath, string spaceId)
        {
            string requstUri = $"{baseAddress}/api/space/{spaceId}/user/fs/file/{targetFilePath}";
            HttpClient client = new HttpClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            HttpResponseMessage response = client.DeleteAsync(requstUri).Result;
            response.EnsureSuccessStatusCode();
            client.Dispose();
        }
    }
}

Usage would look something like the following:

public static void TestDeleteFile()
{
    string baseAddress = @"https://my.machine1.net";
    string userName = "MyEmail";
    string password = "MyPassword";
    string clientName = "ResourceOwnerClientId";
    string clientSecret = "CLIENTSECRET";
    string spaceId = "mytestspace";
    string remoteFilePath = @"MyFolder/TestFile.txt";
    JObject token = Authenticate(baseAddress, userName, password, clientName, clientSecret);
    string accessToken = (string)token["access_token"];
    DeleteUserStoreFile(accessToken, baseAddress, remoteFilePath, spaceId);
}

PowerShell

Since this technique uses the Invoke-RestMethod then remember that the format of the authentication token is $global:bearer from the section Authentication.

Download delete-file.ps1

function RemoveFile($sasHost, $destFileUrlPath, $bearerToken)
{
  <#
    .SYNOPSIS
        Remove files from ShareASpace Nova
    .DESCRIPTION
        fails if the file does not exist on the server
    .EXAMPLE
        Remove a file from the Collection files in the root
        RemoveFile "essevm540.es.eurostep.com" "/api/collection/fs/file/eraseme.txt" $global:bearer
    .EXAMPLE
        Remove a file from the Collection files in a sub-directory called AutoImport.
        RemoveFile "essevm540.es.eurostep.com" "/api/collection/fs/file/AutoImport/eraseme.txt" $global:bearer
    .EXAMPLE
        Remove a file from the User files in a sub-directory called AutoImport
        RemoveFile "essevm540.es.eurostep.com" "/api/space/MySpaceName/user/fs/file/AutoImport/eraseme.txt" $global:bearer
    .EXAMPLE
        Remove a file from the space files in a sub-directory called AutoImport
        RemoveFile "essevm540.es.eurostep.com" "/api/space/MySpaceName/fs/file/AutoImport/eraseme.txt" $global:bearer
    .PARAMETER sasHost
        The FQDN of your host server.
    .PARAMETER destFileUrlPath
        A url that contains the information on where in the unmanaged file system you want to remove the file. It may include the Space name. 
        It looks like either
        /api/space/[SpaceName]/[user or participant or 'nothing for space' or participant/OEM]/fs/file/[subdirectory(one or more)]/[filename]
        or
        /api/collection/fs/file/[subdirectory(one or more)]/[filename]
    .PARAMETER bearer
        The authentication bearer received from a request for autentication.This must have the format @{ "Authorization" = ("Bearer", $global:bearerToken -join " ") } 
  #>
    try{
        $headers = @{"Authorization"="Bearer $bearerToken";}
        $requestUri = "https://{0}{1}" -f $sasHost, $destFileUrlPath
        $response = Invoke-RestMethod -Method Delete -Uri $requestUri -Headers $headers
    }
    catch{
        $statusCode = $_.Exception.Response.StatusCode.value__
        $statusDescription = $_.Exception.Response.StatusDescription
        Write-Host "$($statusDescription)($($statusCode)); Uri: $($requestUri); Response: $($_.Exception)"
    }    
}

RemoveFile "my.machine1.net" "/api/space/mytestspace/user/fs/file/UploadFolder/TestFile.txt" $global:bearerToken

Download sample


The examples above is collected in a project that can be downloaded.

Download Code