Update
All checks were successful
Build & Deploy PLDpro.Web Test to 192.168.1.100 / build-and-deploy (push) Successful in 1m13s
All checks were successful
Build & Deploy PLDpro.Web Test to 192.168.1.100 / build-and-deploy (push) Successful in 1m13s
This commit is contained in:
@@ -46,6 +46,7 @@
|
|||||||
<MudStack Row="true" Spacing="2">
|
<MudStack Row="true" Spacing="2">
|
||||||
<MudText Typo="Typo.h6">Objekte in '@selectedBucket'</MudText>
|
<MudText Typo="Typo.h6">Objekte in '@selectedBucket'</MudText>
|
||||||
<MudSpacer />
|
<MudSpacer />
|
||||||
|
<MudTextField @bind-Value="uploadPath" Placeholder="Pfad (optional, z.B. docs/2026)" Variant="Variant.Outlined" />
|
||||||
<InputFile OnChange="OnFilesSelected" />
|
<InputFile OnChange="OnFilesSelected" />
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
@@ -54,11 +55,24 @@
|
|||||||
<MudTh>Key</MudTh>
|
<MudTh>Key</MudTh>
|
||||||
<MudTh>Größe</MudTh>
|
<MudTh>Größe</MudTh>
|
||||||
<MudTh>Geändert</MudTh>
|
<MudTh>Geändert</MudTh>
|
||||||
|
<MudTh></MudTh>
|
||||||
|
<MudTh></MudTh>
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd DataLabel="Key">@context.Key</MudTd>
|
<MudTd DataLabel="Key">@context.Key</MudTd>
|
||||||
<MudTd DataLabel="Größe">@context.Size</MudTd>
|
<MudTd DataLabel="Größe">@context.Size</MudTd>
|
||||||
<MudTd DataLabel="Geändert">@context.LastModified</MudTd>
|
<MudTd DataLabel="Geändert">@context.LastModified</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudButton Variant="Variant.Text" Color="Color.Primary" Href="@GetDownloadUrl(context.Key)" Target="_blank" StartIcon="@Icons.Material.Filled.Download">Download</MudButton>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
@* Name = letzter Segmentteil des Keys *@
|
||||||
|
@{
|
||||||
|
var fileName = context.Key.Split('/', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? context.Key;
|
||||||
|
var encodedName = Uri.EscapeDataString(fileName);
|
||||||
|
}
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Href="@($"/api/storage/buckets/{selectedBucket}/files/{encodedName}/download")" Target="_blank" StartIcon="@Icons.Material.Filled.Download">Download by Name</MudButton>
|
||||||
|
</MudTd>
|
||||||
</RowTemplate>
|
</RowTemplate>
|
||||||
</MudTable>
|
</MudTable>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
@@ -72,6 +86,7 @@
|
|||||||
private List<ObjectVm>? objects;
|
private List<ObjectVm>? objects;
|
||||||
private string? selectedBucket;
|
private string? selectedBucket;
|
||||||
private string newBucketName = "";
|
private string newBucketName = "";
|
||||||
|
private string? uploadPath; // optionaler Pfad
|
||||||
private const long StreamLimit = 512L * 1024 * 1024; // 512 MB (Program.cs erhöht Multipart-Limit)
|
private const long StreamLimit = 512L * 1024 * 1024; // 512 MB (Program.cs erhöht Multipart-Limit)
|
||||||
|
|
||||||
private HttpClient? Http;
|
private HttpClient? Http;
|
||||||
@@ -116,10 +131,22 @@
|
|||||||
using var content = new MultipartFormDataContent();
|
using var content = new MultipartFormDataContent();
|
||||||
content.Add(new StreamContent(stream), "file", file.Name);
|
content.Add(new StreamContent(stream), "file", file.Name);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(uploadPath))
|
||||||
|
content.Add(new StringContent(uploadPath!), "path");
|
||||||
|
|
||||||
var resp = await Http.PostAsync($"/api/storage/buckets/{selectedBucket}/upload", content);
|
var resp = await Http.PostAsync($"/api/storage/buckets/{selectedBucket}/upload", content);
|
||||||
resp.EnsureSuccessStatusCode();
|
resp.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
// Refresh list
|
// Refresh list
|
||||||
objects = await Http.GetFromJsonAsync<List<ObjectVm>>($"/api/storage/buckets/{selectedBucket}/objects") ?? new();
|
objects = await Http.GetFromJsonAsync<List<ObjectVm>>($"/api/storage/buckets/{selectedBucket}/objects") ?? new();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
private string GetDownloadUrl(string key)
|
||||||
|
{
|
||||||
|
// URL-encode für sichere Übergabe in Catch-all Route
|
||||||
|
var encodedKey = Uri.EscapeDataString(key);
|
||||||
|
return $"/api/storage/buckets/{selectedBucket}/download/{encodedKey}";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,6 @@
|
|||||||
<PackageReference Include="OpenIddict.AspNetCore" Version="7.2.0" />
|
<PackageReference Include="OpenIddict.AspNetCore" Version="7.2.0" />
|
||||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="7.2.0" />
|
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="7.2.0" />
|
||||||
<PackageReference Include="OpenIddict.Validation.AspNetCore" Version="7.2.0" />
|
<PackageReference Include="OpenIddict.Validation.AspNetCore" Version="7.2.0" />
|
||||||
|
<PackageReference Include="MySqlConnector" Version="2.*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
74
Program.cs
74
Program.cs
@@ -1,17 +1,20 @@
|
|||||||
using Amazon.Runtime;
|
using Amazon.Runtime;
|
||||||
using Amazon.S3;
|
using Amazon.S3;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MudBlazor.Services;
|
using MudBlazor.Services;
|
||||||
using Pldpro.Web.Components;
|
using Pldpro.Web.Components;
|
||||||
using Pldpro.Web.Components.Pages;
|
using Pldpro.Web.Components.Pages;
|
||||||
using Pldpro.Web.Models;
|
using Pldpro.Web.Models;
|
||||||
using Pldpro.Web.Services;
|
using Pldpro.Web.Services;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Mime;
|
||||||
using System.Net.NetworkInformation;
|
using System.Net.NetworkInformation;
|
||||||
using System.Runtime.Intrinsics.Arm;
|
using System.Runtime.Intrinsics.Arm;
|
||||||
using static MudBlazor.CategoryTypes;
|
using static MudBlazor.CategoryTypes;
|
||||||
using static MudBlazor.Colors;
|
using static MudBlazor.Colors;
|
||||||
using System.Net.Http;
|
|
||||||
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -27,6 +30,10 @@ builder.Services.AddRazorComponents()
|
|||||||
// HttpClient-Fabrik für serverseitige Komponenten
|
// HttpClient-Fabrik für serverseitige Komponenten
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
// MySQL Repository
|
||||||
|
builder.Services.AddSingleton<IStorageMetadataRepository, StorageMetadataRepository>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- S3 / RustFS Settings binding ---
|
// --- S3 / RustFS Settings binding ---
|
||||||
builder.Services.Configure<S3Settings>(builder.Configuration.GetSection("S3"));
|
builder.Services.Configure<S3Settings>(builder.Configuration.GetSection("S3"));
|
||||||
@@ -55,6 +62,15 @@ builder.Services.AddScoped<IStorageService, S3StorageService>();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|
||||||
|
// Schema sicherstellen (einmalig beim Start)
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var repo = scope.ServiceProvider.GetRequiredService<IStorageMetadataRepository>();
|
||||||
|
await repo.EnsureSchemaAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
@@ -99,18 +115,66 @@ storage.MapGet("/buckets/{bucket}/objects", async (IStorageService svc, string b
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Datei in Bucket hochladen (Form-Data: file)
|
// Datei in Bucket hochladen (Form-Data: file)
|
||||||
storage.MapPost("/buckets/{bucket}/upload", async (HttpRequest req, IStorageService svc, string bucket) =>
|
storage.MapPost("/buckets/{bucket}/upload", async (HttpRequest req, IStorageService svc, IStorageMetadataRepository meta, string bucket, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
if (!req.HasFormContentType) return Results.BadRequest("Multipart/form-data expected");
|
if (!req.HasFormContentType) return Results.BadRequest("Multipart/form-data expected");
|
||||||
var form = await req.ReadFormAsync();
|
var form = await req.ReadFormAsync();
|
||||||
var file = form.Files["file"];
|
var file = form.Files["file"];
|
||||||
if (file is null) return Results.BadRequest("'file' missing");
|
if (file is null) return Results.BadRequest("'file' missing");
|
||||||
|
|
||||||
await using var stream = file.OpenReadStream(); // Streamlimit über FormOptions konfiguriert
|
var path = form["path"].ToString(); // optional, z.B. "docs/2026"
|
||||||
await svc.UploadObjectAsync(bucket, file.FileName, stream, file.ContentType ?? "application/octet-stream");
|
path = string.IsNullOrWhiteSpace(path) ? null : path!.Trim().Trim('/');
|
||||||
return Results.Ok();
|
|
||||||
|
|
||||||
|
//await using var stream = file.OpenReadStream(); // Streamlimit über FormOptions konfiguriert
|
||||||
|
// await svc.UploadObjectAsync(bucket, file.FileName, stream, file.ContentType ?? "application/octet-stream");
|
||||||
|
|
||||||
|
// Key bauen: optionaler Pfad + Dateiname
|
||||||
|
var key = string.IsNullOrWhiteSpace(path) ? file.FileName : $"{path}/{file.FileName}";
|
||||||
|
await using var stream = file.OpenReadStream();
|
||||||
|
var contentType = file.ContentType ?? "application/octet-stream";
|
||||||
|
await svc.UploadObjectAsync(bucket, key, stream, contentType, ct);
|
||||||
|
|
||||||
|
// Metadaten persistieren
|
||||||
|
await meta.UpsertAsync(bucket, file.FileName, path, key, file.Length, contentType, ct);
|
||||||
|
|
||||||
|
return Results.Ok(new { bucket, file = file.FileName, path, key });
|
||||||
|
|
||||||
}).DisableAntiforgery(); // für Blazor XHR Upload
|
}).DisableAntiforgery(); // für Blazor XHR Upload
|
||||||
|
|
||||||
|
|
||||||
|
// Objekt herunterladen
|
||||||
|
storage.MapGet("/buckets/{bucket}/download/{*key}", async (IStorageService svc,string bucket,string key,CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
// key ist als Catch-all {*key} definiert, damit auch Keys mit "/" (Prefix/Ordner) funktionieren.
|
||||||
|
var(stream, contentType, length) = await svc.GetObjectAsync(bucket, key, ct);
|
||||||
|
|
||||||
|
// Dateiname aus Key ableiten:
|
||||||
|
var fileName = key.Split('/', StringSplitOptions.RemoveEmptyEntries).LastOrDefault() ?? key;
|
||||||
|
|
||||||
|
return Results.File(
|
||||||
|
fileStream: stream,
|
||||||
|
contentType: contentType,
|
||||||
|
fileDownloadName: fileName,
|
||||||
|
enableRangeProcessing: true, // erlaubt Resume/Teildownloads
|
||||||
|
lastModified: null, // optional: Last-Modified selbst setzen
|
||||||
|
entityTag: null); // optional: ETag setzen
|
||||||
|
});
|
||||||
|
|
||||||
|
storage.MapGet("/buckets/{bucket}/files/{fileName}/download", async (
|
||||||
|
IStorageService svc,
|
||||||
|
IStorageMetadataRepository meta,
|
||||||
|
string bucket,
|
||||||
|
string fileName,
|
||||||
|
CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
var entry = await meta.TryGetAsync(bucket, fileName, ct);
|
||||||
|
if (entry is null) return Results.NotFound($"No metadata for {bucket}/{fileName}");
|
||||||
|
|
||||||
|
var (stream, contentType, _) = await svc.GetObjectAsync(bucket, entry.Key, ct);
|
||||||
|
return Results.File(stream, contentType, fileName, enableRangeProcessing: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
BIN
Program.txt
BIN
Program.txt
Binary file not shown.
@@ -9,4 +9,5 @@ public interface IStorageService
|
|||||||
Task CreateBucketAsync(string bucketName, CancellationToken ct = default);
|
Task CreateBucketAsync(string bucketName, CancellationToken ct = default);
|
||||||
Task<IEnumerable<ObjectItem>> ListObjectsAsync(string bucket, CancellationToken ct = default);
|
Task<IEnumerable<ObjectItem>> ListObjectsAsync(string bucket, CancellationToken ct = default);
|
||||||
Task UploadObjectAsync(string bucket, string key, Stream content, string contentType, CancellationToken ct = default);
|
Task UploadObjectAsync(string bucket, string key, Stream content, string contentType, CancellationToken ct = default);
|
||||||
|
Task<(Stream Stream, string ContentType, long? ContentLength)> GetObjectAsync(string bucket, string key, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ public sealed class S3StorageService(IAmazonS3 s3) : IStorageService
|
|||||||
BucketName = bucket,
|
BucketName = bucket,
|
||||||
ContinuationToken = token
|
ContinuationToken = token
|
||||||
}, ct);
|
}, ct);
|
||||||
|
if (resp.S3Objects != null)
|
||||||
|
{
|
||||||
items.AddRange(resp.S3Objects.Select(o => new ObjectItem(o.Key, o.Size, o.LastModified)));
|
items.AddRange(resp.S3Objects.Select(o => new ObjectItem(o.Key, o.Size, o.LastModified)));
|
||||||
token = (bool)resp.IsTruncated ? resp.NextContinuationToken : null;
|
token = (bool)resp.IsTruncated ? resp.NextContinuationToken : null;
|
||||||
|
}
|
||||||
} while (token is not null);
|
} while (token is not null);
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
@@ -52,4 +54,24 @@ public sealed class S3StorageService(IAmazonS3 s3) : IStorageService
|
|||||||
};
|
};
|
||||||
await _s3.PutObjectAsync(req, ct);
|
await _s3.PutObjectAsync(req, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<(Stream Stream, string ContentType, long? ContentLength)> GetObjectAsync(
|
||||||
|
string bucket, string key, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var resp = await _s3.GetObjectAsync(new GetObjectRequest
|
||||||
|
{
|
||||||
|
BucketName = bucket,
|
||||||
|
Key = key
|
||||||
|
}, ct);
|
||||||
|
|
||||||
|
// ResponseStream NICHT kopieren, sondern direkt zurückgeben (Server streamt es weiter)
|
||||||
|
var contentType = string.IsNullOrWhiteSpace(resp.Headers.ContentType)
|
||||||
|
? "application/octet-stream"
|
||||||
|
: resp.Headers.ContentType;
|
||||||
|
|
||||||
|
long? len = resp.Headers.ContentLength >= 0 ? resp.Headers.ContentLength : null;
|
||||||
|
return (resp.ResponseStream, contentType, len);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
20
Services/Storage/IStorageMetadataRepository.cs
Normal file
20
Services/Storage/IStorageMetadataRepository.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
namespace Pldpro.Web.Services;
|
||||||
|
|
||||||
|
public interface IStorageMetadataRepository
|
||||||
|
{
|
||||||
|
Task EnsureSchemaAsync(CancellationToken ct = default);
|
||||||
|
Task UpsertAsync(string bucket, string fileName, string? path, string key, long? size, string? contentType, CancellationToken ct = default);
|
||||||
|
Task<StorageObject?> TryGetAsync(string bucket, string fileName, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record StorageObject(
|
||||||
|
long Id,
|
||||||
|
string Bucket,
|
||||||
|
string FileName,
|
||||||
|
string? Path,
|
||||||
|
string Key,
|
||||||
|
long? Size,
|
||||||
|
string? ContentType,
|
||||||
|
DateTime CreatedUtc
|
||||||
|
);
|
||||||
93
Services/Storage/StorageMetaDataRepository.cs
Normal file
93
Services/Storage/StorageMetaDataRepository.cs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
|
||||||
|
using MySqlConnector;
|
||||||
|
using System.Data;
|
||||||
|
|
||||||
|
namespace Pldpro.Web.Services;
|
||||||
|
|
||||||
|
public sealed class StorageMetadataRepository : IStorageMetadataRepository
|
||||||
|
{
|
||||||
|
private readonly string _connStr;
|
||||||
|
public StorageMetadataRepository(IConfiguration cfg) =>
|
||||||
|
_connStr = cfg.GetConnectionString("StorageDb") ?? throw new InvalidOperationException("ConnectionStrings:StorageDb missing");
|
||||||
|
|
||||||
|
public async Task EnsureSchemaAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
CREATE TABLE IF NOT EXISTS storage_objects (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
bucket VARCHAR(63) NOT NULL,
|
||||||
|
file_name VARCHAR(255) NOT NULL,
|
||||||
|
path VARCHAR(768) NULL,
|
||||||
|
s3_key VARCHAR(1024) NOT NULL,
|
||||||
|
size BIGINT NULL,
|
||||||
|
content_type VARCHAR(255) NULL,
|
||||||
|
created_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_bucket_file (bucket, file_name),
|
||||||
|
INDEX ix_bucket_path (bucket, path(255))
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var conn = new MySqlConnection(_connStr);
|
||||||
|
await conn.OpenAsync(ct);
|
||||||
|
await using var cmd = new MySqlCommand(sql, conn);
|
||||||
|
await cmd.ExecuteNonQueryAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpsertAsync(string bucket, string fileName, string? path, string key, long? size, string? contentType, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
INSERT INTO storage_objects (bucket, file_name, path, s3_key, size, content_type)
|
||||||
|
VALUES (@bucket, @name, @path, @key, @size, @ct)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
path = VALUES(path),
|
||||||
|
s3_key = VALUES(s3_key),
|
||||||
|
size = VALUES(size),
|
||||||
|
content_type = VALUES(content_type);
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var conn = new MySqlConnection(_connStr);
|
||||||
|
await conn.OpenAsync(ct);
|
||||||
|
await using var cmd = new MySqlCommand(sql, conn);
|
||||||
|
cmd.Parameters.AddWithValue("@bucket", bucket);
|
||||||
|
cmd.Parameters.AddWithValue("@name", fileName);
|
||||||
|
cmd.Parameters.AddWithValue("@path", (object?)path ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@key", key);
|
||||||
|
cmd.Parameters.AddWithValue("@size", (object?)size ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@ct", (object?)contentType ?? DBNull.Value);
|
||||||
|
await cmd.ExecuteNonQueryAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<StorageObject?> TryGetAsync(string bucket, string fileName, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT id, bucket, file_name, path, s3_key, size, content_type, created_utc
|
||||||
|
FROM storage_objects
|
||||||
|
WHERE bucket = @bucket AND file_name = @name
|
||||||
|
LIMIT 1;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var conn = new MySqlConnection(_connStr);
|
||||||
|
await conn.OpenAsync(ct);
|
||||||
|
|
||||||
|
await using var cmd = new MySqlCommand(sql, conn);
|
||||||
|
cmd.Parameters.AddWithValue("@bucket", bucket);
|
||||||
|
cmd.Parameters.AddWithValue("@name", fileName);
|
||||||
|
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SingleRow, ct);
|
||||||
|
if (await reader.ReadAsync(ct))
|
||||||
|
{
|
||||||
|
return new StorageObject(
|
||||||
|
reader.GetInt64(0),
|
||||||
|
reader.GetString(1),
|
||||||
|
reader.GetString(2),
|
||||||
|
reader.IsDBNull(3) ? null : reader.GetString(3),
|
||||||
|
reader.GetString(4),
|
||||||
|
reader.IsDBNull(5) ? null : reader.GetInt64(5),
|
||||||
|
reader.IsDBNull(6) ? null : reader.GetString(6),
|
||||||
|
reader.GetDateTime(7)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,10 @@
|
|||||||
"UseHttp": true,
|
"UseHttp": true,
|
||||||
"ForcePathStyle": true,
|
"ForcePathStyle": true,
|
||||||
"DefaultBucketPrefix": "pld-" // optional
|
"DefaultBucketPrefix": "pld-" // optional
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"StorageDb": "Server=192.168.1.101;Port=3306;Database=pld_storage;User Id=pld_user;Password=pld_user;TreatTinyAsBoolean=false;SslMode=None;CharSet=utf8mb4"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user