Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions src/TableauSharp/Common/Http/TableauPublishContentBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Net.Http.Headers;
using System.Security;
using System.Text;

namespace TableauSharp.Common.Http;

public static class TableauPublishContentBuilder
{
private static readonly HashSet<string> WorkbookExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".twb",
".twbx"
};

private static readonly HashSet<string> DataSourceExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".hyper",
".tds",
".tdsx",
".tde"
};

public static async Task<HttpContent> CreateWorkbookContentAsync(
string name,
string projectId,
string filePath,
CancellationToken cancellationToken)
{
ValidatePublishRequest(name, projectId, filePath, WorkbookExtensions, "Workbook");

var payload = $"""
<tsRequest>
<workbook name="{Escape(name)}">
<project id="{Escape(projectId)}" />
</workbook>
</tsRequest>
""";

return await CreateMultipartContentAsync(
payload,
"tableau_workbook",
filePath,
cancellationToken);
}

public static async Task<HttpContent> CreateDataSourceContentAsync(
string name,
string projectId,
string? description,
string filePath,
CancellationToken cancellationToken)
{
ValidatePublishRequest(name, projectId, filePath, DataSourceExtensions, "Data source");

var descriptionAttribute = string.IsNullOrWhiteSpace(description)
? string.Empty
: $" description=\"{Escape(description)}\"";
var payload = $"""
<tsRequest>
<datasource name="{Escape(name)}"{descriptionAttribute}>
<project id="{Escape(projectId)}" />
</datasource>
</tsRequest>
""";

return await CreateMultipartContentAsync(
payload,
"tableau_datasource",
filePath,
cancellationToken);
}

private static async Task<HttpContent> CreateMultipartContentAsync(
string payload,
string filePartName,
string filePath,
CancellationToken cancellationToken)
{
var content = new MultipartContent("mixed");

var payloadContent = new StringContent(payload, Encoding.UTF8, "text/xml");
payloadContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
{
Name = "\"request_payload\""
};
content.Add(payloadContent);

var fileContent = new ByteArrayContent(await File.ReadAllBytesAsync(filePath, cancellationToken));
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
{
Name = $"\"{filePartName}\"",
FileName = $"\"{Path.GetFileName(filePath)}\""
};
content.Add(fileContent);

return content;
}

private static void ValidatePublishRequest(
string name,
string projectId,
string filePath,
HashSet<string> allowedExtensions,
string resourceName)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException($"{resourceName} name is required.", nameof(name));
}

if (string.IsNullOrWhiteSpace(projectId))
{
throw new ArgumentException("Project id is required.", nameof(projectId));
}

if (string.IsNullOrWhiteSpace(filePath))
{
throw new ArgumentException("File path is required.", nameof(filePath));
}

if (!File.Exists(filePath))
{
throw new FileNotFoundException("Publish file was not found.", filePath);
}

var extension = Path.GetExtension(filePath);
if (!allowedExtensions.Contains(extension))
{
throw new NotSupportedException($"{resourceName} file extension '{extension}' is not supported.");
}
}

private static string Escape(string value)
=> SecurityElement.Escape(value) ?? string.Empty;
}
91 changes: 37 additions & 54 deletions src/TableauSharp/DataSources/Services/DataSourceService.cs
Original file line number Diff line number Diff line change
@@ -1,45 +1,30 @@
using Microsoft.Extensions.Options;
using System.Text.Json;
using TableauSharp.Common.Helper;
using TableauSharp.Common.Http;
using TableauSharp.DataSources.Models;
using TableauSharp.Settings;

namespace TableauSharp.DataSources.Services;

public class DataSourceService : IDataSourceService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ITableauTokenProvider _tokenProvider;
private readonly TableauAuthOptions _authOptions;
private readonly TableauOptions _tableauOptions;
private readonly ITableauRequestBuilder _requestBuilder;

public DataSourceService(
IHttpClientFactory httpClientFactory,
ITableauTokenProvider tokenProvider,
IOptions<TableauAuthOptions> authOptions,
IOptions<TableauOptions> tableauOptions)
ITableauRequestBuilder requestBuilder)
{
_httpClientFactory = httpClientFactory;
_tokenProvider = tokenProvider;
_authOptions = authOptions.Value;
_tableauOptions = tableauOptions.Value;
_requestBuilder = requestBuilder;
}

private HttpClient CreateClient()
public async Task<IEnumerable<TableauDataSource>> GetAllAsync(CancellationToken cancellationToken = default)
{
var client = _httpClientFactory.CreateClient("TableauClient");
client.BaseAddress = new Uri($"{_tableauOptions.Server}/api/{_tableauOptions.Version}/sites/{_authOptions.SiteContentUrl}/");
client.DefaultRequestHeaders.Add("X-Tableau-Auth", _tokenProvider.GetToken());
return client;
}

public async Task<IEnumerable<TableauDataSource>> GetAllAsync()
{
using var client = CreateClient();
var response = await client.GetAsync("datasources");
using var request = _requestBuilder.CreateSiteRequest(HttpMethod.Get, "datasources");
var response = await client.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();

var json = await response.Content.ReadAsStringAsync();
var json = await response.Content.ReadAsStringAsync(cancellationToken);
using var doc = JsonDocument.Parse(json);

var dataSources = new List<TableauDataSource>();
Expand All @@ -65,13 +50,14 @@ public async Task<IEnumerable<TableauDataSource>> GetAllAsync()
return dataSources;
}

public async Task<TableauDataSource> GetByIdAsync(string dataSourceId)
public async Task<TableauDataSource> GetByIdAsync(string dataSourceId, CancellationToken cancellationToken = default)
{
using var client = CreateClient();
var response = await client.GetAsync($"datasources/{dataSourceId}");
var client = _httpClientFactory.CreateClient("TableauClient");
using var request = _requestBuilder.CreateSiteRequest(HttpMethod.Get, $"datasources/{dataSourceId}");
var response = await client.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();

var json = await response.Content.ReadAsStringAsync();
var json = await response.Content.ReadAsStringAsync(cancellationToken);
using var doc = JsonDocument.Parse(json);
var ds = doc.RootElement.GetProperty("datasource");

Expand All @@ -89,30 +75,25 @@ public async Task<TableauDataSource> GetByIdAsync(string dataSourceId)
};
}

public async Task<TableauDataSource> PublishAsync(DataSourcePublishRequest request)
public async Task<TableauDataSource> PublishAsync(DataSourcePublishRequest request, CancellationToken cancellationToken = default)
{
using var client = CreateClient();
using var form = new MultipartFormDataContent();

form.Add(new StringContent(request.ProjectId), "projectId");
form.Add(new StringContent(request.Name), "datasourceName");
form.Add(new StringContent(request.Overwrite.ToString().ToLower()), "overwrite");

if (!string.IsNullOrEmpty(request.Description))
{
form.Add(new StringContent(request.Description), "description");
}

// Attach data source file
var fileBytes = await File.ReadAllBytesAsync(request.FilePath);
var fileContent = new ByteArrayContent(fileBytes);
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
form.Add(fileContent, "tableau_datasource", Path.GetFileName(request.FilePath));

var response = await client.PostAsync("datasources", form);
var client = _httpClientFactory.CreateClient("TableauClient");
ArgumentNullException.ThrowIfNull(request);

using var content = await TableauPublishContentBuilder.CreateDataSourceContentAsync(
request.Name,
request.ProjectId,
request.Description,
request.FilePath,
cancellationToken);
using var httpRequest = _requestBuilder.CreateSiteRequest(
HttpMethod.Post,
$"datasources?overwrite={request.Overwrite.ToString().ToLowerInvariant()}");
httpRequest.Content = content;
var response = await client.SendAsync(httpRequest, cancellationToken);
response.EnsureSuccessStatusCode();

var json = await response.Content.ReadAsStringAsync();
var json = await response.Content.ReadAsStringAsync(cancellationToken);
using var doc = JsonDocument.Parse(json);
var ds = doc.RootElement.GetProperty("datasource");

Expand All @@ -130,17 +111,19 @@ public async Task<TableauDataSource> PublishAsync(DataSourcePublishRequest reque
};
}

public async Task DeleteAsync(string dataSourceId)
public async Task DeleteAsync(string dataSourceId, CancellationToken cancellationToken = default)
{
using var client = CreateClient();
var response = await client.DeleteAsync($"datasources/{dataSourceId}");
var client = _httpClientFactory.CreateClient("TableauClient");
using var request = _requestBuilder.CreateSiteRequest(HttpMethod.Delete, $"datasources/{dataSourceId}");
var response = await client.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
}

public async Task RefreshAsync(string dataSourceId)
public async Task RefreshAsync(string dataSourceId, CancellationToken cancellationToken = default)
{
using var client = CreateClient();
var response = await client.PostAsync($"datasources/{dataSourceId}/refresh", null);
var client = _httpClientFactory.CreateClient("TableauClient");
using var request = _requestBuilder.CreateSiteRequest(HttpMethod.Post, $"datasources/{dataSourceId}/refresh");
var response = await client.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
}
}
10 changes: 5 additions & 5 deletions src/TableauSharp/DataSources/Services/IDataSourceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,25 @@ public interface IDataSourceService
/// <summary>
/// Get all data sources in the site
/// </summary>
Task<IEnumerable<TableauDataSource>> GetAllAsync();
Task<IEnumerable<TableauDataSource>> GetAllAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Get a specific data source by ID
/// </summary>
Task<TableauDataSource> GetByIdAsync(string dataSourceId);
Task<TableauDataSource> GetByIdAsync(string dataSourceId, CancellationToken cancellationToken = default);

/// <summary>
/// Publish a data source to a project
/// </summary>
Task<TableauDataSource> PublishAsync(DataSourcePublishRequest request);
Task<TableauDataSource> PublishAsync(DataSourcePublishRequest request, CancellationToken cancellationToken = default);

/// <summary>
/// Delete a data source
/// </summary>
Task DeleteAsync(string dataSourceId);
Task DeleteAsync(string dataSourceId, CancellationToken cancellationToken = default);

/// <summary>
/// Refresh a data source extract
/// </summary>
Task RefreshAsync(string dataSourceId);
Task RefreshAsync(string dataSourceId, CancellationToken cancellationToken = default);
}
18 changes: 9 additions & 9 deletions src/TableauSharp/Permissions/Services/IPermissionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,45 +10,45 @@ public interface IPermissionService
/// <summary>
/// Get permissions for a workbook
/// </summary>
Task<IEnumerable<TableauPermission>> GetWorkbookPermissionsAsync(string workbookId);
Task<IEnumerable<TableauPermission>> GetWorkbookPermissionsAsync(string workbookId, CancellationToken cancellationToken = default);

/// <summary>
/// Get permissions for a project
/// </summary>
Task<IEnumerable<TableauPermission>> GetProjectPermissionsAsync(string projectId);
Task<IEnumerable<TableauPermission>> GetProjectPermissionsAsync(string projectId, CancellationToken cancellationToken = default);

/// <summary>
/// Get permissions for a data source
/// </summary>
Task<IEnumerable<TableauPermission>> GetDataSourcePermissionsAsync(string dataSourceId);
Task<IEnumerable<TableauPermission>> GetDataSourcePermissionsAsync(string dataSourceId, CancellationToken cancellationToken = default);

/// <summary>
/// Add or update permission capability for a workbook
/// </summary>
Task AddWorkbookPermissionAsync(string workbookId, TableauPermission permission);
Task AddWorkbookPermissionAsync(string workbookId, TableauPermission permission, CancellationToken cancellationToken = default);

/// <summary>
/// Add or update permission capability for a project
/// </summary>
Task AddProjectPermissionAsync(string projectId, TableauPermission permission);
Task AddProjectPermissionAsync(string projectId, TableauPermission permission, CancellationToken cancellationToken = default);

/// <summary>
/// Add or update permission capability for a data source
/// </summary>
Task AddDataSourcePermissionAsync(string dataSourceId, TableauPermission permission);
Task AddDataSourcePermissionAsync(string dataSourceId, TableauPermission permission, CancellationToken cancellationToken = default);

/// <summary>
/// Delete permission for a workbook
/// </summary>
Task DeleteWorkbookPermissionAsync(string workbookId, string granteeId, string granteeType, string capability);
Task DeleteWorkbookPermissionAsync(string workbookId, string granteeId, string granteeType, string capability, CancellationToken cancellationToken = default);

/// <summary>
/// Delete permission for a project
/// </summary>
Task DeleteProjectPermissionAsync(string projectId, string granteeId, string granteeType, string capability);
Task DeleteProjectPermissionAsync(string projectId, string granteeId, string granteeType, string capability, CancellationToken cancellationToken = default);

/// <summary>
/// Delete permission for a data source
/// </summary>
Task DeleteDataSourcePermissionAsync(string dataSourceId, string granteeId, string granteeType, string capability);
Task DeleteDataSourcePermissionAsync(string dataSourceId, string granteeId, string granteeType, string capability, CancellationToken cancellationToken = default);
}
Loading
Loading