diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 741d94a61..b9d89f055 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -12,6 +12,11 @@
+
+
+
+
+
+
+
-
-
@@ -143,7 +148,7 @@
-
+
@@ -177,6 +182,9 @@
+
+
+
diff --git a/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/indexLayout.xml b/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/indexLayout.xml
index 7b08163ce..0e04cc861 100644
--- a/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/indexLayout.xml
+++ b/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/indexLayout.xml
@@ -1,7 +1,9 @@
-
+
+ ../../nix
+
diff --git a/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/sqldialects.xml b/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/sqldialects.xml
new file mode 100644
index 000000000..0f3b7c205
--- /dev/null
+++ b/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/sqldialects.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/FilesystemFileSource.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/FilesystemFileSource.cs
new file mode 100644
index 000000000..a3199071f
--- /dev/null
+++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/FilesystemFileSource.cs
@@ -0,0 +1,41 @@
+using ArcaneLibs;
+
+namespace Spacebar.Interop.Cdn.Abstractions;
+
+public class FilesystemFileSource(string baseUrl) : IFileSource {
+ private static LruFileCache _cache = new(100 * 1024 * 1024); // 100 MB
+
+ private readonly StreamingHttpClient _httpClient = new() {
+ BaseAddress = new Uri(baseUrl)
+ };
+
+ public string BaseUrl => baseUrl;
+
+ public async Task GetFile(string path, CancellationToken? cancellationToken = null) {
+ var res = await _cache.GetOrAdd(path, async () => {
+ var res = await _httpClient.SendUnhandledAsync(new(HttpMethod.Get, path), cancellationToken);
+ res.EnsureSuccessStatusCode();
+ var ms = new MemoryStream();
+ await res.Content.CopyToAsync(ms);
+ return new LruFileCache.Entry {
+ Data = ms.ToArray(),
+ MimeType = res.Content.Headers.ContentType?.MediaType ?? "application/octet-stream"
+ };
+ });
+
+ return new() {
+ Stream = new MemoryStream(res.Data),
+ MimeType = res.MimeType
+ };
+ }
+
+ public Task FileExists(string path, CancellationToken? cancellationToken = null) {
+ return Task.FromResult(File.Exists(Path.Combine(baseUrl, path)));
+ }
+
+ // private string GetMimeType(Stream stream)
+ // {
+ // using var mic = new MagickImageCollection(stream);
+ // return Mimes.GetMime(mic.First().Format);
+ // }
+}
\ No newline at end of file
diff --git a/extra/admin-api/Spacebar.Cdn/Services/IFileSource.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/IFileSource.cs
similarity index 51%
rename from extra/admin-api/Spacebar.Cdn/Services/IFileSource.cs
rename to extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/IFileSource.cs
index e10328727..2ffa9acf5 100644
--- a/extra/admin-api/Spacebar.Cdn/Services/IFileSource.cs
+++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/IFileSource.cs
@@ -1,10 +1,9 @@
-using ImageMagick;
-
-namespace Spacebar.AdminApi.TestClient.Services.Services;
+namespace Spacebar.Interop.Cdn.Abstractions;
public interface IFileSource {
public string BaseUrl { get; }
public Task GetFile(string path, CancellationToken? cancellationToken = null);
+ public Task FileExists(string path, CancellationToken? cancellationToken = null);
}
public class FileInfo : IDisposable, IAsyncDisposable {
@@ -28,23 +27,4 @@ public class FileInfo : IDisposable, IAsyncDisposable {
await DisposeAsyncCore();
GC.SuppressFinalize(this);
}
-
- public async Task ToMagickImageCollectionAsync() {
- var ms = new MemoryStream();
- Stream.Position = 0;
- await Stream.CopyToAsync(ms);
- ms.Position = 0;
- var img = MimeType switch {
- "image/apng" => new MagickImageCollection(ms, MagickFormat.APng),
- _ => new MagickImageCollection(ms)
- };
-
- // if (img.First().Format == MagickFormat.Png) {
- // img.Dispose();
- // ms.Position = 0;
- // img = new MagickImageCollection(ms, MagickFormat.APng);
- // }
-
- return img;
- }
}
\ No newline at end of file
diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/LruFileCache.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/LruFileCache.cs
new file mode 100644
index 000000000..3bd4ec095
--- /dev/null
+++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/LruFileCache.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Spacebar.Interop.Cdn.Abstractions;
+
+public class LruFileCache(int maxSizeBytes) {
+ private readonly Dictionary _entries = new();
+
+ public async Task GetOrAdd(string key, Func> factory) {
+ if (_entries.TryGetValue(key, out var entry)) {
+ entry.LastAccessed = DateTimeOffset.UtcNow;
+ return entry;
+ }
+
+ entry = await factory();
+ if (entry.Data.Length > 0 && entry.Data.Length <= maxSizeBytes)
+ _entries[key] = entry;
+
+ if (_entries.Sum(kv => kv.Value.Data.Length) > maxSizeBytes) {
+ var oldestKey = _entries.OrderBy(kv => kv.Value.LastAccessed).First().Key;
+ _entries.Remove(oldestKey);
+ }
+
+ return entry;
+ }
+
+ public class Entry {
+ public DateTimeOffset LastAccessed { get; set; }
+ public byte[] Data { get; set; }
+ public string MimeType { get; set; }
+ }
+}
+
diff --git a/extra/admin-api/Spacebar.Cdn/Services/ProxyFileSource.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/ProxyFileSource.cs
similarity index 76%
rename from extra/admin-api/Spacebar.Cdn/Services/ProxyFileSource.cs
rename to extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/ProxyFileSource.cs
index bc6ab2ae0..af7cecf2d 100644
--- a/extra/admin-api/Spacebar.Cdn/Services/ProxyFileSource.cs
+++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/ProxyFileSource.cs
@@ -1,4 +1,6 @@
-namespace Spacebar.AdminApi.TestClient.Services.Services;
+using ArcaneLibs;
+
+namespace Spacebar.Interop.Cdn.Abstractions;
public class ProxyFileSource(string baseUrl) : IFileSource {
private static LruFileCache _cache = new(100 * 1024 * 1024); // 100 MB
@@ -26,4 +28,9 @@ public class ProxyFileSource(string baseUrl) : IFileSource {
MimeType = res.MimeType
};
}
+
+ public async Task FileExists(string path, CancellationToken? cancellationToken = null) {
+ var res = await _httpClient.SendUnhandledAsync(new(HttpMethod.Head, path), cancellationToken);
+ return res.IsSuccessStatusCode;
+ }
}
\ No newline at end of file
diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/Spacebar.Interop.Cdn.Abstractions.csproj b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/Spacebar.Interop.Cdn.Abstractions.csproj
new file mode 100644
index 000000000..daf6beb6c
--- /dev/null
+++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/Spacebar.Interop.Cdn.Abstractions.csproj
@@ -0,0 +1,12 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/deps.json b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/deps.json
new file mode 100644
index 000000000..0637a088a
--- /dev/null
+++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/deps.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/Spacebar.Interop.Replication.RabbitMq.csproj b/extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/Spacebar.Interop.Replication.RabbitMq.csproj
index ea3a4079c..7b463eaa8 100644
--- a/extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/Spacebar.Interop.Replication.RabbitMq.csproj
+++ b/extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/Spacebar.Interop.Replication.RabbitMq.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/extra/admin-api/Models/Spacebar.Models.Db/Spacebar.Models.Db.csproj b/extra/admin-api/Models/Spacebar.Models.Db/Spacebar.Models.Db.csproj
index 320274c05..3a588b36c 100644
--- a/extra/admin-api/Models/Spacebar.Models.Db/Spacebar.Models.Db.csproj
+++ b/extra/admin-api/Models/Spacebar.Models.Db/Spacebar.Models.Db.csproj
@@ -8,7 +8,9 @@
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/extra/admin-api/Models/Spacebar.Models.Db/deps.json b/extra/admin-api/Models/Spacebar.Models.Db/deps.json
index 9088516af..c4a9edf55 100644
--- a/extra/admin-api/Models/Spacebar.Models.Db/deps.json
+++ b/extra/admin-api/Models/Spacebar.Models.Db/deps.json
@@ -5,39 +5,14 @@
"hash": "sha256-EXvojddPu+9JKgOG9NSQgUTfWq1RpOYw7adxDPKDJ6o="
},
{
- "pname": "Microsoft.Build",
- "version": "17.7.2",
- "hash": "sha256-k35nFdPxC8t0zAltVSmAJtsepp/ubNIjPOsJ6k8jSqM="
+ "pname": "Microsoft.Build.Framework",
+ "version": "17.11.31",
+ "hash": "sha256-YS4oASrmC5dmZrx5JPS7SfKmUpIJErlUpVDsU3VrfFE="
},
{
"pname": "Microsoft.Build.Framework",
- "version": "17.14.28",
- "hash": "sha256-7RzEyIipumafwLW1xN1q23114NafG6PT0+RADElNsiM="
- },
- {
- "pname": "Microsoft.Build.Framework",
- "version": "17.7.2",
- "hash": "sha256-fNWmVQYFTJDveAGmxEdNqJRAczV6+Ep8RA8clKBJFqw="
- },
- {
- "pname": "Microsoft.Build.Tasks.Core",
- "version": "17.14.28",
- "hash": "sha256-M9zRXYijH2HtLlRXbrUK1a1LQ9zkT+DC9ZmMiiVZwv0="
- },
- {
- "pname": "Microsoft.Build.Tasks.Core",
- "version": "17.7.2",
- "hash": "sha256-OrV/qWgZHzGlNUmaSfX5wDBcmg1aQeF3/OUHpSH+uZU="
- },
- {
- "pname": "Microsoft.Build.Utilities.Core",
- "version": "17.14.28",
- "hash": "sha256-VFfO+UpyTpw2X/qiCCOCYzvMLuu7B+XVSSpJZQLkPzU="
- },
- {
- "pname": "Microsoft.Build.Utilities.Core",
- "version": "17.7.2",
- "hash": "sha256-oatF0KfuP1nb4+OLNKg2/R/ZLO4EiACaO5leaxMEY4A="
+ "version": "18.0.2",
+ "hash": "sha256-fO31KAdDs2J0RUYD1ov9UB3ucsbALan7K0YdWW+yg7A="
},
{
"pname": "Microsoft.CodeAnalysis.Analyzers",
@@ -46,28 +21,28 @@
},
{
"pname": "Microsoft.CodeAnalysis.Common",
- "version": "4.14.0",
- "hash": "sha256-ne/zxH3GqoGB4OemnE8oJElG5mai+/67ASaKqwmL2BE="
+ "version": "5.0.0",
+ "hash": "sha256-g4ALvBSNyHEmSb1l5TFtWW7zEkiRmhqLx4XWZu9sr2U="
},
{
"pname": "Microsoft.CodeAnalysis.CSharp",
- "version": "4.14.0",
- "hash": "sha256-5Mzj3XkYYLkwDWh17r1NEXSbXwwWYQPiOmkSMlgo1JY="
+ "version": "5.0.0",
+ "hash": "sha256-ctBCkQGFpH/xT5rRE3xibu9YxPD108RuC4a4Z25koG8="
},
{
"pname": "Microsoft.CodeAnalysis.CSharp.Workspaces",
- "version": "4.14.0",
- "hash": "sha256-aNbV1a0yYBs0fpQawG6LXcbyoE8en+YFSpV5vcYE4J4="
+ "version": "5.0.0",
+ "hash": "sha256-yWVcLt/f2CouOfFy966glGdtSFy+RcgrU1dd9UtlL/Q="
},
{
"pname": "Microsoft.CodeAnalysis.Workspaces.Common",
- "version": "4.14.0",
- "hash": "sha256-0YfeaJe01WBUm9avy4a8FacQJXA1NkpnDpiXu4yz88I="
+ "version": "5.0.0",
+ "hash": "sha256-Bir5e1gEhgQQ6upQmVKQHAKLRfenAu60DAzNupNnZsQ="
},
{
"pname": "Microsoft.CodeAnalysis.Workspaces.MSBuild",
- "version": "4.14.0",
- "hash": "sha256-5SJfpRqzqCK0UbkmAaJpA/r1XJb0YAriMMeQHYC4d+o="
+ "version": "5.0.0",
+ "hash": "sha256-+58+iqTayTiE0pDaog1U8mjaDA8bNNDLA8gjCQZZudo="
},
{
"pname": "Microsoft.EntityFrameworkCore",
@@ -86,29 +61,19 @@
},
{
"pname": "Microsoft.EntityFrameworkCore.Design",
- "version": "10.0.1",
- "hash": "sha256-GGNZIGNEMhSGaMRFkRN4bOuCUBs5YVnX8klXarm319U="
+ "version": "10.0.2",
+ "hash": "sha256-bTShsGux0y/49PIIMb/4ZX3x5+rPacvT5/NcooNCI1Y="
},
{
"pname": "Microsoft.EntityFrameworkCore.Relational",
- "version": "10.0.0",
- "hash": "sha256-vOP2CE5YA551BlpbOuIy6RuAiAEPEpCVS1cEE33/zN4="
- },
- {
- "pname": "Microsoft.EntityFrameworkCore.Relational",
- "version": "10.0.1",
- "hash": "sha256-zLgxr/iW9HP8Fip1IDgr7X0Ar8OWKDvVmoEt65gG6VY="
+ "version": "10.0.2",
+ "hash": "sha256-Y4jPpoYhKizg5wF6QfkBX4sYlE2FU1bYhfoDN3xkhKM="
},
{
"pname": "Microsoft.Extensions.Caching.Abstractions",
"version": "10.0.2",
"hash": "sha256-nKmQuZTt1g5/8gBajo7wdCV64kdCucdiQR8JTt7ZZb0="
},
- {
- "pname": "Microsoft.Extensions.Caching.Memory",
- "version": "10.0.1",
- "hash": "sha256-Qb7xK6VEZDas0lJFaW1suKdFjtkSYwLHHxkQEfWIU2A="
- },
{
"pname": "Microsoft.Extensions.Caching.Memory",
"version": "10.0.2",
@@ -116,8 +81,8 @@
},
{
"pname": "Microsoft.Extensions.Configuration.Abstractions",
- "version": "10.0.1",
- "hash": "sha256-s4PDp+vtzdxKIxnOT3+dDRoTDopyl8kqmmw4KDnkOtQ="
+ "version": "10.0.2",
+ "hash": "sha256-P+0kaDGO+xB9KxF9eWHDJ4hzi05sUGM/uMNEX5NdBTE="
},
{
"pname": "Microsoft.Extensions.DependencyInjection",
@@ -136,13 +101,8 @@
},
{
"pname": "Microsoft.Extensions.DependencyModel",
- "version": "10.0.1",
- "hash": "sha256-XIj2jEURe25YA4RhBSuCqQpic0YP+TZaO/dbBPCjad8="
- },
- {
- "pname": "Microsoft.Extensions.Logging",
- "version": "10.0.1",
- "hash": "sha256-zuLP3SIpCToMOlIPOEv3Kq8y/minecd8k8GSkxFo13E="
+ "version": "10.0.2",
+ "hash": "sha256-w/dGIjtZiGH+KW3969BPOdQpQEV+WB7RPTa2MK2DavE="
},
{
"pname": "Microsoft.Extensions.Logging",
@@ -179,11 +139,6 @@
"version": "9.0.0",
"hash": "sha256-DT5euAQY/ItB5LPI8WIp6Dnd0lSvBRP35vFkOXC68ck="
},
- {
- "pname": "Microsoft.Extensions.Primitives",
- "version": "10.0.1",
- "hash": "sha256-EXmukq09erT4s+miQpBSYy3IY4HxxKlwEPL43/KoyEc="
- },
{
"pname": "Microsoft.Extensions.Primitives",
"version": "10.0.2",
@@ -195,14 +150,9 @@
"hash": "sha256-ZNLusK1CRuq5BZYZMDqaz04PIKScE2Z7sS2tehU7EJs="
},
{
- "pname": "Microsoft.NET.StringTools",
- "version": "17.14.28",
- "hash": "sha256-UzREyvDxkiOQ4cEOQ5UCjkwXGrldIDCcbefECTPGjXI="
- },
- {
- "pname": "Microsoft.NET.StringTools",
- "version": "17.7.2",
- "hash": "sha256-hQE07TCgcQuyu9ZHVq2gPDb0+xe8ECJUdrgh17bJP4o="
+ "pname": "Microsoft.VisualStudio.SolutionPersistence",
+ "version": "1.0.52",
+ "hash": "sha256-KZGPtOXe6Hv8RrkcsgoLKTRyaCScIpQEa2NhNB3iOXw="
},
{
"pname": "Mono.TextTemplating",
@@ -229,16 +179,6 @@
"version": "6.0.0",
"hash": "sha256-uPetUFZyHfxjScu5x4agjk9pIhbCkt5rG4Axj25npcQ="
},
- {
- "pname": "System.CodeDom",
- "version": "7.0.0",
- "hash": "sha256-7IPt39cY+0j0ZcRr/J45xPtEjnSXdUJ/5ai3ebaYQiE="
- },
- {
- "pname": "System.CodeDom",
- "version": "9.0.0",
- "hash": "sha256-578lcBgswW0eM16r0EnJzfGodPx86RxxFoZHc2PSzsw="
- },
{
"pname": "System.Composition",
"version": "9.0.0",
@@ -268,80 +208,5 @@
"pname": "System.Composition.TypedParts",
"version": "9.0.0",
"hash": "sha256-F5fpTUs3Rr7yP/NyIzr+Xn5NdTXXp8rrjBnF9UBBUog="
- },
- {
- "pname": "System.Configuration.ConfigurationManager",
- "version": "7.0.0",
- "hash": "sha256-SgBexTTjRn23uuXvkzO0mz0qOfA23MiS4Wv+qepMLZE="
- },
- {
- "pname": "System.Configuration.ConfigurationManager",
- "version": "9.0.0",
- "hash": "sha256-+pLnTC0YDP6Kjw5DVBiFrV/Q3x5is/+6N6vAtjvhVWk="
- },
- {
- "pname": "System.Diagnostics.EventLog",
- "version": "9.0.0",
- "hash": "sha256-tPvt6yoAp56sK/fe+/ei8M65eavY2UUhRnbrREj/Ems="
- },
- {
- "pname": "System.Formats.Nrbf",
- "version": "9.0.0",
- "hash": "sha256-c4qf6CocQUZB0ySGQd8s15PXY7xfrjQqMGXxkwytKyw="
- },
- {
- "pname": "System.Reflection.MetadataLoadContext",
- "version": "7.0.0",
- "hash": "sha256-VYl6SFD130K9Aw4eJH16ApJ9Sau4Xu0dcxEip2veuTI="
- },
- {
- "pname": "System.Resources.Extensions",
- "version": "9.0.0",
- "hash": "sha256-y2gLEMuAy6QfEyNJxABC/ayMWGnwlpX735jsUQLktho="
- },
- {
- "pname": "System.Security.Cryptography.Pkcs",
- "version": "7.0.0",
- "hash": "sha256-3J3vL9hcKSuZjT2GKappa2A9p2xJm1nH2asTNAl8ZCA="
- },
- {
- "pname": "System.Security.Cryptography.Pkcs",
- "version": "7.0.2",
- "hash": "sha256-qS5Z/Yo8J+f3ExVX5Qkcpj1Z57oUZqz5rWa1h5bVpl8="
- },
- {
- "pname": "System.Security.Cryptography.Pkcs",
- "version": "9.0.0",
- "hash": "sha256-AjG14mGeSc2Ka4QSelGBM1LrGBW3VJX60lnihKyJjGY="
- },
- {
- "pname": "System.Security.Cryptography.ProtectedData",
- "version": "9.0.0",
- "hash": "sha256-gPgPU7k/InTqmXoRzQfUMEKL3QuTnOKowFqmXTnWaBQ="
- },
- {
- "pname": "System.Security.Cryptography.Xml",
- "version": "7.0.1",
- "hash": "sha256-CH8+JVC8LyCSW75/6ZQ7ecMbSOAE1c16z4dG8JTp01w="
- },
- {
- "pname": "System.Security.Cryptography.Xml",
- "version": "9.0.0",
- "hash": "sha256-SQJWwAFrJUddEU6JiZB52FM9tGjRlJAYH8oYVzG5IJU="
- },
- {
- "pname": "System.Security.Permissions",
- "version": "7.0.0",
- "hash": "sha256-DOFoX+AKRmrkllykHheR8FfUXYx/Ph+I/HYuReQydXI="
- },
- {
- "pname": "System.Security.Permissions",
- "version": "9.0.0",
- "hash": "sha256-BFrA9ottmQtLIAiKiGRbfSUpzNJwuaOCeFRDN4Z0ku0="
- },
- {
- "pname": "System.Windows.Extensions",
- "version": "9.0.0",
- "hash": "sha256-RErD+Ju15qtnwdwB7E0SjjJGAnhXwJyC7UPcl24Z3Vs="
}
]
diff --git a/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj b/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj
index 53b438f3f..6c1b2e18a 100644
--- a/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj
+++ b/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj
@@ -4,21 +4,22 @@
net10.0
enable
enable
+ true
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
diff --git a/extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs b/extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs
index 13b5da68f..2f0149265 100644
--- a/extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs
+++ b/extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs
@@ -1,8 +1,10 @@
using ArcaneLibs.Extensions.Streams;
using Microsoft.AspNetCore.Mvc;
using Spacebar.AdminApi.TestClient.Services.Services;
+using Spacebar.Cdn.Extensions;
+using Spacebar.Interop.Cdn.Abstractions;
-namespace Spacebar.AdminApi.TestClient.Services.Controllers;
+namespace Spacebar.Cdn.Controllers;
[ApiController]
public class GetImageController(LruFileCache lfc, IFileSource fs, DiscordImageResizeService dirs) : ControllerBase {
diff --git a/extra/admin-api/Spacebar.Cdn/Controllers/Internal/GetImageController.cs b/extra/admin-api/Spacebar.Cdn/Controllers/Internal/GetImageController.cs
index 4063e79c6..dd8d4255e 100644
--- a/extra/admin-api/Spacebar.Cdn/Controllers/Internal/GetImageController.cs
+++ b/extra/admin-api/Spacebar.Cdn/Controllers/Internal/GetImageController.cs
@@ -1,8 +1,11 @@
+using ArcaneLibs.Collections;
using ImageMagick;
using Microsoft.AspNetCore.Mvc;
using Spacebar.AdminApi.TestClient.Services.Services;
+using Spacebar.Cdn.Extensions;
+using Spacebar.Interop.Cdn.Abstractions;
-namespace Spacebar.AdminApi.TestClient.Services.Controllers;
+namespace Spacebar.Cdn.Controllers.Internal;
[ApiController]
public class IsPixelArtController(LruFileCache lfc, IFileSource fs, PixelArtDetectionService pads, DiscordImageResizeService dirs) : ControllerBase {
diff --git a/extra/admin-api/Spacebar.Cdn/Extensions/FileSourceExtensions.cs b/extra/admin-api/Spacebar.Cdn/Extensions/FileSourceExtensions.cs
new file mode 100644
index 000000000..218ba6a23
--- /dev/null
+++ b/extra/admin-api/Spacebar.Cdn/Extensions/FileSourceExtensions.cs
@@ -0,0 +1,25 @@
+using ImageMagick;
+using FileInfo = Spacebar.Interop.Cdn.Abstractions.FileInfo;
+
+namespace Spacebar.Cdn.Extensions;
+
+public static class FileSourceExtensions {
+ public static async Task ToMagickImageCollectionAsync(this FileInfo fileInfo) {
+ var ms = new MemoryStream();
+ fileInfo.Stream.Position = 0;
+ await fileInfo.Stream.CopyToAsync(ms);
+ ms.Position = 0;
+ var img = fileInfo.MimeType switch {
+ "image/apng" => new MagickImageCollection(ms, MagickFormat.APng),
+ _ => new MagickImageCollection(ms)
+ };
+
+ // if (img.First().Format == MagickFormat.Png) {
+ // img.Dispose();
+ // ms.Position = 0;
+ // img = new MagickImageCollection(ms, MagickFormat.APng);
+ // }
+
+ return img;
+ }
+}
\ No newline at end of file
diff --git a/extra/admin-api/Spacebar.Cdn/Program.cs b/extra/admin-api/Spacebar.Cdn/Program.cs
index 34dc8b13d..f7a319d30 100644
--- a/extra/admin-api/Spacebar.Cdn/Program.cs
+++ b/extra/admin-api/Spacebar.Cdn/Program.cs
@@ -1,6 +1,9 @@
+using ArcaneLibs;
using ImageMagick;
-using Spacebar.AdminApi.TestClient.Services;
+using Microsoft.EntityFrameworkCore;
using Spacebar.AdminApi.TestClient.Services.Services;
+using Spacebar.Interop.Cdn.Abstractions;
+using Spacebar.Models.Db.Contexts;
var builder = WebApplication.CreateBuilder(args);
@@ -10,6 +13,12 @@ builder.Services.AddSingleton(new LruFileCache(1*1024*1024*1024));
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+builder.Services.AddDbContextPool(options => {
+ options
+ .UseNpgsql(builder.Configuration.GetConnectionString("Spacebar"))
+ .EnableDetailedErrors();
+});
+
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
diff --git a/extra/admin-api/Spacebar.Cdn/Properties/launchSettings.json b/extra/admin-api/Spacebar.Cdn/Properties/launchSettings.json
index 5205c88e7..b9fe4cc30 100644
--- a/extra/admin-api/Spacebar.Cdn/Properties/launchSettings.json
+++ b/extra/admin-api/Spacebar.Cdn/Properties/launchSettings.json
@@ -10,6 +10,16 @@
"ASPNETCORE_ENVIRONMENT": "Development",
"LD_LIBRARY_PATH": "/home/Rory/git/spacebar/server-master/extra/admin-api/Spacebar.Cdn/result-lib/lib/"
}
+ },
+ "Local": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5114",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Local",
+ "LD_LIBRARY_PATH": "/home/Rory/git/spacebar/server-master/extra/admin-api/Spacebar.Cdn/result-lib/lib/"
+ }
}
}
}
diff --git a/extra/admin-api/Spacebar.Cdn/Services/CdnStorageRebuildService.cs b/extra/admin-api/Spacebar.Cdn/Services/CdnStorageRebuildService.cs
new file mode 100644
index 000000000..0cf0ef1ab
--- /dev/null
+++ b/extra/admin-api/Spacebar.Cdn/Services/CdnStorageRebuildService.cs
@@ -0,0 +1,5 @@
+namespace Spacebar.AdminApi.TestClient.Services.Services;
+
+public class CdnFileStorageRebuildService() {
+
+}
\ No newline at end of file
diff --git a/extra/admin-api/Spacebar.Cdn/Services/LruFileCache.cs b/extra/admin-api/Spacebar.Cdn/Services/LruFileCache.cs
deleted file mode 100644
index 3a25b1e4a..000000000
--- a/extra/admin-api/Spacebar.Cdn/Services/LruFileCache.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-namespace Spacebar.AdminApi.TestClient.Services.Services;
-
-public class LruFileCache(int maxSizeBytes) {
- private readonly Dictionary _entries = new();
-
- public async Task GetOrAdd(string key, Func> factory) {
- if (_entries.TryGetValue(key, out var entry)) {
- entry.LastAccessed = DateTimeOffset.UtcNow;
- return entry;
- }
-
- entry = await factory();
- if (entry.Data.Length > 0)
- _entries[key] = entry;
-
- if (_entries.Sum(kv => kv.Value.Data.Length) > maxSizeBytes) {
- var oldestKey = _entries.OrderBy(kv => kv.Value.LastAccessed).First().Key;
- _entries.Remove(oldestKey);
- }
-
- return entry;
- }
-
- public class Entry {
- public DateTimeOffset LastAccessed { get; set; }
- public byte[] Data { get; set; }
- public string MimeType { get; set; }
- }
-}
-
-public class LruCache(int maxItems) {
- private readonly Dictionary _items = new();
-
- public async Task GetOrAddAsync(string key, Func> factory) {
- if (_items.TryGetValue(key, out var cacheItem)) {
- cacheItem.LastAccessed = DateTimeOffset.UtcNow;
- return cacheItem.Value;
- }
-
- var value = await factory();
- _items[key] = new CacheItem {
- Value = value,
- LastAccessed = DateTimeOffset.UtcNow
- };
-
- if (_items.Count > maxItems) {
- var oldestKey = _items.OrderBy(kv => kv.Value.LastAccessed).First().Key;
- _items.Remove(oldestKey);
- }
-
- return value;
- }
-
- private class CacheItem {
- public T Value { get; set; }
- public DateTimeOffset LastAccessed { get; set; }
- }
-}
\ No newline at end of file
diff --git a/extra/admin-api/Spacebar.Cdn/Spacebar.Cdn.csproj b/extra/admin-api/Spacebar.Cdn/Spacebar.Cdn.csproj
index 35d71d45f..6a9ec98fd 100644
--- a/extra/admin-api/Spacebar.Cdn/Spacebar.Cdn.csproj
+++ b/extra/admin-api/Spacebar.Cdn/Spacebar.Cdn.csproj
@@ -7,13 +7,14 @@
-
+
-
+
+
diff --git a/extra/admin-api/Spacebar.Cdn/StreamingHttpClient.cs b/extra/admin-api/Spacebar.Cdn/StreamingHttpClient.cs
deleted file mode 100644
index 8090c0532..000000000
--- a/extra/admin-api/Spacebar.Cdn/StreamingHttpClient.cs
+++ /dev/null
@@ -1,304 +0,0 @@
-#define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances
-// #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging
-using System.Data;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Net.Http.Headers;
-using System.Reflection;
-using System.Text;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using ArcaneLibs;
-using ArcaneLibs.Extensions;
-
-namespace Spacebar.AdminApi.TestClient.Services;
-
-#if SINGLE_HTTPCLIENT
-// TODO: Add URI wrapper for
-public class StreamingHttpClient {
- private static readonly HttpClient Client;
-
- static StreamingHttpClient() {
- try {
- var handler = new SocketsHttpHandler {
- PooledConnectionLifetime = TimeSpan.FromMinutes(15),
- MaxConnectionsPerServer = 4096,
- EnableMultipleHttp2Connections = true
- };
- Client = new HttpClient(handler) {
- DefaultRequestVersion = new Version(3, 0),
- Timeout = TimeSpan.FromDays(1)
- };
- }
- catch (PlatformNotSupportedException e) {
- Console.WriteLine("Failed to create HttpClient with connection pooling, continuing without connection pool!");
- Console.WriteLine("Original exception (safe to ignore!):");
- Console.WriteLine(e);
-
- Client = new HttpClient {
- DefaultRequestVersion = new Version(3, 0)
- };
- }
- catch (Exception e) {
- Console.WriteLine("Failed to create HttpClient:");
- Console.WriteLine(e);
- throw;
- }
- }
-
-#if SYNC_HTTPCLIENT
- internal SemaphoreSlim _rateLimitSemaphore { get; } = new(1, 1);
-#endif
-
- public static bool LogRequests = true;
- public Dictionary AdditionalQueryParameters { get; set; } = new();
-
- public Uri? BaseAddress { get; set; }
-
- // default headers, not bound to client
- public HttpRequestHeaders DefaultRequestHeaders { get; set; } =
- typeof(HttpRequestHeaders).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, [], null)?.Invoke([]) as HttpRequestHeaders ??
- throw new InvalidOperationException("Failed to create HttpRequestHeaders");
-
- private static JsonSerializerOptions GetJsonSerializerOptions(JsonSerializerOptions? options = null) {
- options ??= new JsonSerializerOptions();
- // options.Converters.Add(new JsonFloatStringConverter());
- // options.Converters.Add(new JsonDoubleStringConverter());
- // options.Converters.Add(new JsonDecimalStringConverter());
- options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
- return options;
- }
-
- public async Task SendUnhandledAsync(HttpRequestMessage request, CancellationToken? cancellationToken) {
- if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null");
- // if (!request.RequestUri.IsAbsoluteUri)
- request.RequestUri = request.RequestUri.EnsureAbsolute(BaseAddress!);
- var swWait = Stopwatch.StartNew();
-#if SYNC_HTTPCLIENT
- await _rateLimitSemaphore.WaitAsync(cancellationToken);
-#endif
-
- if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null");
- if (!request.RequestUri.IsAbsoluteUri)
- request.RequestUri = new Uri(BaseAddress ?? throw new InvalidOperationException("Relative URI passed, but no BaseAddress is specified!"), request.RequestUri);
- swWait.Stop();
- var swExec = Stopwatch.StartNew();
-
- foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value);
- foreach (var (key, value) in DefaultRequestHeaders) {
- if (request.Headers.Contains(key)) continue;
- request.Headers.Add(key, value);
- }
-
- request.Options.Set(new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"), true);
-
- if (LogRequests)
- Console.WriteLine("Sending " + request.Summarise(includeHeaders: true, includeQuery: true, includeContentIfText: false, hideHeaders: ["Accept"]));
-
- HttpResponseMessage? responseMessage;
- try {
- responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken ?? CancellationToken.None);
- }
- catch (Exception e) {
- if (e is TaskCanceledException or TimeoutException) {
- if (request.Method == HttpMethod.Get && !(cancellationToken?.IsCancellationRequested ?? false)) {
- await Task.Delay(Random.Shared.Next(500, 2500), cancellationToken ?? CancellationToken.None);
- request.ResetSendStatus();
- return await SendAsync(request, cancellationToken ?? CancellationToken.None);
- }
- }
- else if (!e.ToString().StartsWith("TypeError: NetworkError"))
- Console.WriteLine(
- $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}");
-
- throw;
- }
-#if SYNC_HTTPCLIENT
- finally {
- _rateLimitSemaphore.Release();
- }
-#endif
-
- // Console.WriteLine($"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.GetContentLength())}, WAIT={swWait.ElapsedMilliseconds}ms, EXEC={swExec.ElapsedMilliseconds}ms)");
- if (LogRequests)
- Console.WriteLine("Received " + responseMessage.Summarise(includeHeaders: true, includeContentIfText: false, hideHeaders: [
- "Server",
- "Date",
- "Transfer-Encoding",
- "Connection",
- "Vary",
- "Content-Length",
- "Access-Control-Allow-Origin",
- "Access-Control-Allow-Methods",
- "Access-Control-Allow-Headers",
- "Access-Control-Expose-Headers",
- "Cache-Control",
- "Cross-Origin-Resource-Policy",
- "X-Content-Security-Policy",
- "Referrer-Policy",
- "X-Robots-Tag",
- "Content-Security-Policy"
- ]));
-
- return responseMessage;
- }
-
- public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) {
- var responseMessage = await SendUnhandledAsync(request, cancellationToken);
- if (responseMessage.IsSuccessStatusCode) return responseMessage;
-
- //retry on gateway timeout
- // if (responseMessage.StatusCode == HttpStatusCode.GatewayTimeout) {
- // request.ResetSendStatus();
- // return await SendAsync(request, cancellationToken);
- // }
-
- //error handling
- var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken);
- if (content.Length == 0)
- throw new DataException("Content was empty");
- // throw new MatrixException() {
- // ErrorCode = "M_UNKNOWN",
- // Error = "Unknown error, server returned no content"
- // };
-
- // if (!content.StartsWith('{')) throw new InvalidDataException("Encountered invalid data:\n" + content);
- if (!content.TrimStart().StartsWith('{')) {
- responseMessage.EnsureSuccessStatusCode();
- throw new InvalidDataException("Encountered invalid data:\n" + content);
- }
- //we have a matrix error
-
- throw new Exception("Unknown http exception");
- // MatrixException? ex;
- // try {
- // ex = JsonSerializer.Deserialize(content);
- // }
- // catch (JsonException e) {
- // throw new LibMatrixException() {
- // ErrorCode = "M_INVALID_JSON",
- // Error = e.Message + "\nBody:\n" + await responseMessage.Content.ReadAsStringAsync(cancellationToken)
- // };
- // }
- //
- // Debug.Assert(ex != null, nameof(ex) + " != null");
- // ex.RawContent = content;
- // // Console.WriteLine($"Failed to send request: {ex}");
- // if (ex.RetryAfterMs is null) throw ex!;
- // //we have a ratelimit error
- // await Task.Delay(ex.RetryAfterMs.Value, cancellationToken);
- request.ResetSendStatus();
- return await SendAsync(request, cancellationToken);
- }
-
- // GetAsync
- public Task GetAsync([StringSyntax("Uri")] string? requestUri, CancellationToken? cancellationToken = null) =>
- SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken ?? CancellationToken.None);
-
- // GetFromJsonAsync
- public async Task TryGetFromJsonAsync(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) {
- try {
- return await GetFromJsonAsync(requestUri, options, cancellationToken);
- }
- catch (JsonException e) {
- Console.WriteLine($"Failed to deserialize response from {requestUri}: {e.Message}");
- return default;
- }
- catch (HttpRequestException e) {
- Console.WriteLine($"Failed to get {requestUri}: {e.Message}");
- return default;
- }
- }
-
- public async Task GetFromJsonAsync(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) {
- options = GetJsonSerializerOptions(options);
- var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
- request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
- var response = await SendAsync(request, cancellationToken);
- response.EnsureSuccessStatusCode();
- await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
-
- return await JsonSerializer.DeserializeAsync(responseStream, options, cancellationToken) ??
- throw new InvalidOperationException("Failed to deserialize response");
- }
-
- // GetStreamAsync
- public async Task GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) {
- var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
- request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
- var response = await SendAsync(request, cancellationToken);
- response.EnsureSuccessStatusCode();
- return await response.Content.ReadAsStreamAsync(cancellationToken);
- }
-
- public async Task PutAsJsonAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null,
- CancellationToken cancellationToken = default) where T : notnull {
- options = GetJsonSerializerOptions(options);
- var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
- request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
- request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options),
- Encoding.UTF8, "application/json");
- return await SendAsync(request, cancellationToken);
- }
-
- public async Task PostAsJsonAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null,
- CancellationToken cancellationToken = default) where T : notnull {
- options ??= new JsonSerializerOptions();
- // options.Converters.Add(new JsonFloatStringConverter());
- // options.Converters.Add(new JsonDoubleStringConverter());
- // options.Converters.Add(new JsonDecimalStringConverter());
- options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
- var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
- request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
- request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options),
- Encoding.UTF8, "application/json");
- return await SendAsync(request, cancellationToken);
- }
-
- public async IAsyncEnumerable GetAsyncEnumerableFromJsonAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, JsonSerializerOptions? options = null) {
- options = GetJsonSerializerOptions(options);
- var res = await GetAsync(requestUri);
- options.PropertyNameCaseInsensitive = true;
- var result = JsonSerializer.DeserializeAsyncEnumerable(await res.Content.ReadAsStreamAsync(), options);
- await foreach (var resp in result) yield return resp;
- }
-
- public static async Task CheckSuccessStatus(string url) {
- //cors causes failure, try to catch
- try {
- var resp = await Client.GetAsync(url);
- return resp.IsSuccessStatusCode;
- }
- catch (Exception e) {
- Console.WriteLine($"Failed to check success status: {e.Message}");
- return false;
- }
- }
-
- public async Task PostAsync(string uri, HttpContent? content, CancellationToken cancellationToken = default) {
- var request = new HttpRequestMessage(HttpMethod.Post, uri) {
- Content = content
- };
- return await SendAsync(request, cancellationToken);
- }
-
- public async Task DeleteAsync(string url) {
- var request = new HttpRequestMessage(HttpMethod.Delete, url);
- return await SendAsync(request);
- }
-
- public async Task DeleteAsJsonAsync(string url, T payload) {
- var request = new HttpRequestMessage(HttpMethod.Delete, url) {
- Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
- };
- return await SendAsync(request);
- }
-
- public async Task PatchAsJsonAsync(string url, T payload) {
- var request = new HttpRequestMessage(new HttpMethod("PATCH"), url) {
- Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
- };
- return await SendAsync(request);
- }
-}
-#endif
\ No newline at end of file
diff --git a/extra/admin-api/SpacebarAdminAPI.sln b/extra/admin-api/SpacebarAdminAPI.sln
index 3ec737fb7..3235d6d2a 100644
--- a/extra/admin-api/SpacebarAdminAPI.sln
+++ b/extra/admin-api/SpacebarAdminAPI.sln
@@ -37,6 +37,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.Interop.Replicatio
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.Interop.Replication.UnixSocket", "Interop\Spacebar.Interop.Replication.UnixSocket\Spacebar.Interop.Replication.UnixSocket.csproj", "{2D8F75C2-C4DC-4DBC-A880-5BF021D637E5}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.Cdn.Fsck", "Utilities\Spacebar.Cdn.Fsck\Spacebar.Cdn.Fsck.csproj", "{05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.Interop.Cdn.Abstractions", "Interop\Spacebar.Interop.Cdn.Abstractions\Spacebar.Interop.Cdn.Abstractions.csproj", "{B494C58D-2D53-49F4-9E3E-1DCF60828802}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -215,6 +219,30 @@ Global
{2D8F75C2-C4DC-4DBC-A880-5BF021D637E5}.Release|x64.Build.0 = Release|Any CPU
{2D8F75C2-C4DC-4DBC-A880-5BF021D637E5}.Release|x86.ActiveCfg = Release|Any CPU
{2D8F75C2-C4DC-4DBC-A880-5BF021D637E5}.Release|x86.Build.0 = Release|Any CPU
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Debug|x64.Build.0 = Debug|Any CPU
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Debug|x86.Build.0 = Debug|Any CPU
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Release|x64.ActiveCfg = Release|Any CPU
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Release|x64.Build.0 = Release|Any CPU
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Release|x86.ActiveCfg = Release|Any CPU
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Release|x86.Build.0 = Release|Any CPU
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Debug|x64.Build.0 = Debug|Any CPU
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Debug|x86.Build.0 = Debug|Any CPU
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Release|x64.ActiveCfg = Release|Any CPU
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Release|x64.Build.0 = Release|Any CPU
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Release|x86.ActiveCfg = Release|Any CPU
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -232,5 +260,7 @@ Global
{6F2A4C4B-8EAA-4469-9320-B52C23D9640F} = {16DBEA54-D51A-4D91-84DF-C701B6B4786F}
{2FAE6018-CF95-4B44-A7F7-D6FAF596076D} = {16DBEA54-D51A-4D91-84DF-C701B6B4786F}
{2D8F75C2-C4DC-4DBC-A880-5BF021D637E5} = {16DBEA54-D51A-4D91-84DF-C701B6B4786F}
+ {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D} = {04787943-EBB6-4DE4-96D5-4CFB4A2CEE99}
+ {B494C58D-2D53-49F4-9E3E-1DCF60828802} = {16DBEA54-D51A-4D91-84DF-C701B6B4786F}
EndGlobalSection
EndGlobal
diff --git a/extra/admin-api/Utilities/DiscordEmojiConverter/DiscordEmojiConverter.csproj b/extra/admin-api/Utilities/DiscordEmojiConverter/DiscordEmojiConverter.csproj
index 72d306b35..7ed79afd1 100644
--- a/extra/admin-api/Utilities/DiscordEmojiConverter/DiscordEmojiConverter.csproj
+++ b/extra/admin-api/Utilities/DiscordEmojiConverter/DiscordEmojiConverter.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj
index 5daabdd0a..546875d23 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Guilds.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Guilds.razor
index f8cc15dd3..b105968e8 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Guilds.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Guilds.razor
@@ -1,6 +1,7 @@
@page "/Guilds"
@using System.Net.Http.Headers
@using System.Reflection
+@using ArcaneLibs
@using Spacebar.Models.AdminApi
@using Spacebar.AdminApi.TestClient.Services
@using ArcaneLibs.Blazor.Components
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClient.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClient.razor
index b70f4efb6..91aec6598 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClient.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClient.razor
@@ -1,5 +1,6 @@
@page "/HttpTestClient"
@using System.Text.Json
+@using ArcaneLibs
@using ArcaneLibs.Blazor.Components
@using ArcaneLibs.Extensions
@using Spacebar.AdminApi.TestClient.Classes.OpenAPI
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor
index 73ce4ec2d..3e6131041 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor
@@ -3,6 +3,7 @@
@using System.Diagnostics.CodeAnalysis
@using System.Net.Http.Headers
@using System.Text.Json.Serialization
+@using ArcaneLibs
@using ArcaneLibs.Blazor.Components
@using ArcaneLibs.Blazor.Components.Services
@using ArcaneLibs.Extensions
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/ServerConfig.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/ServerConfig.razor
index 7a83bd52a..4ecbf9d36 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/ServerConfig.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/ServerConfig.razor
@@ -2,6 +2,7 @@
@inject Config Config
@using System.Net.Http.Headers
@using System.Text.Json
+@using ArcaneLibs
@using Spacebar.AdminApi.TestClient.Services
@using Spacebar.ConfigModel.Extensions
Server Config
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Users.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Users.razor
index 939c5dca4..fa8ad23ad 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Users.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Users.razor
@@ -1,6 +1,7 @@
@page "/Users"
@using System.Net.Http.Headers
@using System.Reflection
+@using ArcaneLibs
@using Spacebar.Models.AdminApi
@using Spacebar.AdminApi.TestClient.Services
@using ArcaneLibs.Blazor.Components
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/UsersDelete.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/UsersDelete.razor
index b2116ebda..e9a59f3b2 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/UsersDelete.razor
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/UsersDelete.razor
@@ -2,6 +2,7 @@
@using System.Net.Http.Headers
@using System.Text.Json
@using System.Text.Json.Nodes
+@using ArcaneLibs
@using ArcaneLibs.Extensions
@using Spacebar.Models.AdminApi
@using Spacebar.AdminApi.TestClient.Services
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs
deleted file mode 100644
index 311eb4579..000000000
--- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs
+++ /dev/null
@@ -1,304 +0,0 @@
-#define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances
-// #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging
-using System.Data;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Net.Http.Headers;
-using System.Reflection;
-using System.Text;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using ArcaneLibs;
-using ArcaneLibs.Extensions;
-
-namespace Spacebar.AdminApi.TestClient.Services;
-
-#if SINGLE_HTTPCLIENT
-// TODO: Add URI wrapper for
-public class StreamingHttpClient {
- private static readonly HttpClient Client;
-
- static StreamingHttpClient() {
- try {
- var handler = new SocketsHttpHandler {
- PooledConnectionLifetime = TimeSpan.FromMinutes(15),
- MaxConnectionsPerServer = 4096,
- EnableMultipleHttp2Connections = true
- };
- Client = new HttpClient(handler) {
- DefaultRequestVersion = new Version(3, 0),
- Timeout = TimeSpan.FromDays(1)
- };
- }
- catch (PlatformNotSupportedException e) {
- Console.WriteLine("Failed to create HttpClient with connection pooling, continuing without connection pool!");
- Console.WriteLine("Original exception (safe to ignore!):");
- Console.WriteLine(e);
-
- Client = new HttpClient {
- DefaultRequestVersion = new Version(3, 0)
- };
- }
- catch (Exception e) {
- Console.WriteLine("Failed to create HttpClient:");
- Console.WriteLine(e);
- throw;
- }
- }
-
-#if SYNC_HTTPCLIENT
- internal SemaphoreSlim _rateLimitSemaphore { get; } = new(1, 1);
-#endif
-
- public static bool LogRequests = true;
- public Dictionary AdditionalQueryParameters { get; set; } = new();
-
- public Uri? BaseAddress { get; set; }
-
- // default headers, not bound to client
- public HttpRequestHeaders DefaultRequestHeaders { get; set; } =
- typeof(HttpRequestHeaders).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, [], null)?.Invoke([]) as HttpRequestHeaders ??
- throw new InvalidOperationException("Failed to create HttpRequestHeaders");
-
- private static JsonSerializerOptions GetJsonSerializerOptions(JsonSerializerOptions? options = null) {
- options ??= new JsonSerializerOptions();
- // options.Converters.Add(new JsonFloatStringConverter());
- // options.Converters.Add(new JsonDoubleStringConverter());
- // options.Converters.Add(new JsonDecimalStringConverter());
- options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
- return options;
- }
-
- public async Task SendUnhandledAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
- if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null");
- // if (!request.RequestUri.IsAbsoluteUri)
- request.RequestUri = request.RequestUri.EnsureAbsolute(BaseAddress!);
- var swWait = Stopwatch.StartNew();
-#if SYNC_HTTPCLIENT
- await _rateLimitSemaphore.WaitAsync(cancellationToken);
-#endif
-
- if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null");
- if (!request.RequestUri.IsAbsoluteUri)
- request.RequestUri = new Uri(BaseAddress ?? throw new InvalidOperationException("Relative URI passed, but no BaseAddress is specified!"), request.RequestUri);
- swWait.Stop();
- var swExec = Stopwatch.StartNew();
-
- foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value);
- foreach (var (key, value) in DefaultRequestHeaders) {
- if (request.Headers.Contains(key)) continue;
- request.Headers.Add(key, value);
- }
-
- request.Options.Set(new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"), true);
-
- if (LogRequests)
- Console.WriteLine("Sending " + request.Summarise(includeHeaders: true, includeQuery: true, includeContentIfText: false, hideHeaders: ["Accept"]));
-
- HttpResponseMessage? responseMessage;
- try {
- responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
- }
- catch (Exception e) {
- if (e is TaskCanceledException or TimeoutException) {
- if (request.Method == HttpMethod.Get && !cancellationToken.IsCancellationRequested) {
- await Task.Delay(Random.Shared.Next(500, 2500), cancellationToken);
- request.ResetSendStatus();
- return await SendAsync(request, cancellationToken);
- }
- }
- else if (!e.ToString().StartsWith("TypeError: NetworkError"))
- Console.WriteLine(
- $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}");
-
- throw;
- }
-#if SYNC_HTTPCLIENT
- finally {
- _rateLimitSemaphore.Release();
- }
-#endif
-
- // Console.WriteLine($"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.GetContentLength())}, WAIT={swWait.ElapsedMilliseconds}ms, EXEC={swExec.ElapsedMilliseconds}ms)");
- if (LogRequests)
- Console.WriteLine("Received " + responseMessage.Summarise(includeHeaders: true, includeContentIfText: false, hideHeaders: [
- "Server",
- "Date",
- "Transfer-Encoding",
- "Connection",
- "Vary",
- "Content-Length",
- "Access-Control-Allow-Origin",
- "Access-Control-Allow-Methods",
- "Access-Control-Allow-Headers",
- "Access-Control-Expose-Headers",
- "Cache-Control",
- "Cross-Origin-Resource-Policy",
- "X-Content-Security-Policy",
- "Referrer-Policy",
- "X-Robots-Tag",
- "Content-Security-Policy"
- ]));
-
- return responseMessage;
- }
-
- public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) {
- var responseMessage = await SendUnhandledAsync(request, cancellationToken);
- if (responseMessage.IsSuccessStatusCode) return responseMessage;
-
- //retry on gateway timeout
- // if (responseMessage.StatusCode == HttpStatusCode.GatewayTimeout) {
- // request.ResetSendStatus();
- // return await SendAsync(request, cancellationToken);
- // }
-
- //error handling
- var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken);
- if (content.Length == 0)
- throw new DataException("Content was empty");
- // throw new MatrixException() {
- // ErrorCode = "M_UNKNOWN",
- // Error = "Unknown error, server returned no content"
- // };
-
- // if (!content.StartsWith('{')) throw new InvalidDataException("Encountered invalid data:\n" + content);
- if (!content.TrimStart().StartsWith('{')) {
- responseMessage.EnsureSuccessStatusCode();
- throw new InvalidDataException("Encountered invalid data:\n" + content);
- }
- //we have a matrix error
-
- throw new Exception("Unknown http exception");
- // MatrixException? ex;
- // try {
- // ex = JsonSerializer.Deserialize(content);
- // }
- // catch (JsonException e) {
- // throw new LibMatrixException() {
- // ErrorCode = "M_INVALID_JSON",
- // Error = e.Message + "\nBody:\n" + await responseMessage.Content.ReadAsStringAsync(cancellationToken)
- // };
- // }
- //
- // Debug.Assert(ex != null, nameof(ex) + " != null");
- // ex.RawContent = content;
- // // Console.WriteLine($"Failed to send request: {ex}");
- // if (ex.RetryAfterMs is null) throw ex!;
- // //we have a ratelimit error
- // await Task.Delay(ex.RetryAfterMs.Value, cancellationToken);
- request.ResetSendStatus();
- return await SendAsync(request, cancellationToken);
- }
-
- // GetAsync
- public Task GetAsync([StringSyntax("Uri")] string? requestUri, CancellationToken? cancellationToken = null) =>
- SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken ?? CancellationToken.None);
-
- // GetFromJsonAsync
- public async Task TryGetFromJsonAsync(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) {
- try {
- return await GetFromJsonAsync(requestUri, options, cancellationToken);
- }
- catch (JsonException e) {
- Console.WriteLine($"Failed to deserialize response from {requestUri}: {e.Message}");
- return default;
- }
- catch (HttpRequestException e) {
- Console.WriteLine($"Failed to get {requestUri}: {e.Message}");
- return default;
- }
- }
-
- public async Task GetFromJsonAsync(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) {
- options = GetJsonSerializerOptions(options);
- var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
- request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
- var response = await SendAsync(request, cancellationToken);
- response.EnsureSuccessStatusCode();
- await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
-
- return await JsonSerializer.DeserializeAsync(responseStream, options, cancellationToken) ??
- throw new InvalidOperationException("Failed to deserialize response");
- }
-
- // GetStreamAsync
- public async Task GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) {
- var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
- request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
- var response = await SendAsync(request, cancellationToken);
- response.EnsureSuccessStatusCode();
- return await response.Content.ReadAsStreamAsync(cancellationToken);
- }
-
- public async Task PutAsJsonAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null,
- CancellationToken cancellationToken = default) where T : notnull {
- options = GetJsonSerializerOptions(options);
- var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
- request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
- request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options),
- Encoding.UTF8, "application/json");
- return await SendAsync(request, cancellationToken);
- }
-
- public async Task PostAsJsonAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null,
- CancellationToken cancellationToken = default) where T : notnull {
- options ??= new JsonSerializerOptions();
- // options.Converters.Add(new JsonFloatStringConverter());
- // options.Converters.Add(new JsonDoubleStringConverter());
- // options.Converters.Add(new JsonDecimalStringConverter());
- options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
- var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
- request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
- request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options),
- Encoding.UTF8, "application/json");
- return await SendAsync(request, cancellationToken);
- }
-
- public async IAsyncEnumerable GetAsyncEnumerableFromJsonAsync([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, JsonSerializerOptions? options = null) {
- options = GetJsonSerializerOptions(options);
- var res = await GetAsync(requestUri);
- options.PropertyNameCaseInsensitive = true;
- var result = JsonSerializer.DeserializeAsyncEnumerable(await res.Content.ReadAsStreamAsync(), options);
- await foreach (var resp in result) yield return resp;
- }
-
- public static async Task CheckSuccessStatus(string url) {
- //cors causes failure, try to catch
- try {
- var resp = await Client.GetAsync(url);
- return resp.IsSuccessStatusCode;
- }
- catch (Exception e) {
- Console.WriteLine($"Failed to check success status: {e.Message}");
- return false;
- }
- }
-
- public async Task PostAsync(string uri, HttpContent? content, CancellationToken cancellationToken = default) {
- var request = new HttpRequestMessage(HttpMethod.Post, uri) {
- Content = content
- };
- return await SendAsync(request, cancellationToken);
- }
-
- public async Task DeleteAsync(string url) {
- var request = new HttpRequestMessage(HttpMethod.Delete, url);
- return await SendAsync(request);
- }
-
- public async Task DeleteAsJsonAsync(string url, T payload) {
- var request = new HttpRequestMessage(HttpMethod.Delete, url) {
- Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
- };
- return await SendAsync(request);
- }
-
- public async Task PatchAsJsonAsync(string url, T payload) {
- var request = new HttpRequestMessage(new HttpMethod("PATCH"), url) {
- Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
- };
- return await SendAsync(request);
- }
-}
-#endif
\ No newline at end of file
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj
index 552dbd3ec..d1656c633 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj
+++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj
@@ -18,8 +18,8 @@
-
-
+
+
diff --git a/extra/admin-api/Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj b/extra/admin-api/Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj
index 5daabdd0a..546875d23 100644
--- a/extra/admin-api/Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj
+++ b/extra/admin-api/Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs
new file mode 100644
index 000000000..c0b0bbc3e
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs
@@ -0,0 +1,117 @@
+using System.Diagnostics;
+using Microsoft.EntityFrameworkCore;
+using Spacebar.Interop.Cdn.Abstractions;
+using Spacebar.Models.Db.Contexts;
+
+namespace Spacebar.Cdn.Fsck;
+
+public class FsckService(ILogger logger, IServiceScopeFactory serviceScopeFactory, IFileSource fs) : IHostedService {
+ private SpacebarDbContext _db = null!;
+
+ public async Task StartAsync(CancellationToken cancellationToken) {
+ var sw = Stopwatch.StartNew();
+ await using var scope = serviceScopeFactory.CreateAsyncScope();
+ _db = scope.ServiceProvider.GetRequiredService();
+ logger.LogInformation("Starting fsck on {source}...", $"{fs.GetType().FullName}({fs.BaseUrl})");
+ await RunFsckAsync("User Avatars", "/avatars", EnumerateUserAvatarFilesAsync());
+ await RunFsckAsync("User Banners", "/banners", EnumerateUserBannerPathsAsync());
+ await RunFsckAsync("Guild Icons", "/icons", EnumerateGuildIconPathsAsync());
+ await RunFsckAsync("Stickers", "/stickers", EnumerateStickerPathsAsync());
+ await RunFsckAsync("Emojis", "/emojis", EnumerateEmojiPathsAsync());
+ logger.LogInformation("Fsck complete in {time}.", sw.Elapsed);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken) { }
+
+ private readonly Stopwatch _lastUpdateSw = Stopwatch.StartNew();
+ private readonly SemaphoreSlim _fsckSemaphore = new SemaphoreSlim(32, 32);
+
+ public struct FsckItem {
+ public string Path;
+ public string ItemId;
+ }
+
+ private async Task RunFsckAsync(string name, string path, IQueryable items) {
+ int i = 0, count = await items.CountAsync();
+ List tasks = [];
+
+ await foreach (var item in items.AsAsyncEnumerable()) {
+ tasks.Add(Task.Run(async () => {
+ await _fsckSemaphore.WaitAsync();
+ if (_lastUpdateSw.ElapsedMilliseconds >= (1000 / 30) || i == 0) {
+ _lastUpdateSw.Restart();
+ Console.Write($"{name} fsck: {i}/{count}: {item.Path,-64}\r");
+ }
+
+ i++;
+ if (!await fs.FileExists(item.Path))
+ logger.LogWarning("{itemType} {itemId} is missing at {path}", name, item.ItemId, item.Path);
+
+ _fsckSemaphore.Release();
+ }));
+ }
+
+ await Task.WhenAll(tasks);
+ logger.LogInformation("Validated {count} items for {path}.", i, path);
+ }
+
+#region User Assets
+
+ public IQueryable EnumerateUserAvatarFilesAsync() =>
+ _db.Users
+ .Where(x => !string.IsNullOrWhiteSpace(x.Avatar))
+ .OrderBy(x => x.Id)
+ .Select(x => new FsckItem {
+ Path = $"/avatars/{x.Id}/{x.Avatar}",
+ ItemId = x.Id
+ });
+
+ public IQueryable EnumerateUserBannerPathsAsync() =>
+ _db.Users
+ .Where(x => !string.IsNullOrWhiteSpace(x.Banner))
+ .OrderBy(x => x.Id)
+ .Select(x => new FsckItem {
+ Path = $"/banners/{x.Id}/{x.Banner}",
+ ItemId = x.Id
+ });
+
+#endregion
+
+#region Guild Assets
+
+ public IQueryable EnumerateGuildIconPathsAsync() =>
+ _db.Guilds
+ .Where(x => !string.IsNullOrWhiteSpace(x.Icon))
+ .OrderBy(x => x.Id)
+ .Select(x => new FsckItem {
+ Path = $"/icons/{x.Id}/{x.Icon}",
+ ItemId = x.Id
+ });
+
+ public IQueryable EnumerateRoleIconPathsAsync() =>
+ _db.Roles
+ .Where(x => !string.IsNullOrWhiteSpace(x.Icon))
+ .OrderBy(x => x.Id)
+ .Select(x => new FsckItem {
+ Path = $"/role-icons/{x.Id}/{x.Icon}",
+ ItemId = x.Id
+ });
+
+ public IQueryable EnumerateStickerPathsAsync() =>
+ _db.Stickers
+ .OrderBy(x => x.Id)
+ .Select(x => new FsckItem {
+ Path = $"/stickers/{x.Id}.png",
+ ItemId = x.Id
+ });
+
+ public IQueryable EnumerateEmojiPathsAsync() =>
+ _db.Emojis
+ .OrderBy(x => x.Id)
+ .Select(x => new FsckItem {
+ Path = $"/emojis/{x.Id}",
+ ItemId = x.Id
+ });
+
+#endregion
+}
\ No newline at end of file
diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Program.cs b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Program.cs
new file mode 100644
index 000000000..15822cc95
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Program.cs
@@ -0,0 +1,21 @@
+using ArcaneLibs;
+using Microsoft.EntityFrameworkCore;
+using Spacebar.Cdn.Fsck;
+using Spacebar.Interop.Cdn.Abstractions;
+using Spacebar.Models.Db.Contexts;
+
+var builder = Host.CreateApplicationBuilder(args);
+builder.Services.AddSingleton(new ProxyFileSource("http://cdn.old.server.spacebar.chat"));
+builder.Services.AddSingleton(new LruFileCache(1*1024*1024*1024));
+builder.Services.AddHostedService();
+
+builder.Services.AddDbContextPool(options => {
+ options
+ .UseNpgsql(builder.Configuration.GetConnectionString("Spacebar"))
+ .EnableDetailedErrors();
+});
+
+StreamingHttpClient.LogRequests = false;
+
+var host = builder.Build();
+host.Start();
\ No newline at end of file
diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Properties/launchSettings.json b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Properties/launchSettings.json
new file mode 100644
index 000000000..d29035162
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Properties/launchSettings.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Development": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "Local": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Local"
+ }
+ }
+ }
+}
diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Spacebar.Cdn.Fsck.csproj b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Spacebar.Cdn.Fsck.csproj
new file mode 100644
index 000000000..e08d3ae87
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Spacebar.Cdn.Fsck.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net10.0
+ enable
+ enable
+ dotnet-Spacebar.Cdn.Fsck-a4aee86f-7e64-4c71-88d7-d2a76cc8b77e
+ True
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.Development.json b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.Development.json
new file mode 100644
index 000000000..b2dcdb674
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.json b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.json
new file mode 100644
index 000000000..b2dcdb674
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/deps.json b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/deps.json
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/deps.json
@@ -0,0 +1 @@
+[]
diff --git a/extra/admin-api/Utilities/Spacebar.CleanSettingsRows/deps.json b/extra/admin-api/Utilities/Spacebar.CleanSettingsRows/deps.json
index 3bf6a98c8..269c9e391 100644
--- a/extra/admin-api/Utilities/Spacebar.CleanSettingsRows/deps.json
+++ b/extra/admin-api/Utilities/Spacebar.CleanSettingsRows/deps.json
@@ -5,39 +5,14 @@
"hash": "sha256-EXvojddPu+9JKgOG9NSQgUTfWq1RpOYw7adxDPKDJ6o="
},
{
- "pname": "Microsoft.Build",
- "version": "17.7.2",
- "hash": "sha256-k35nFdPxC8t0zAltVSmAJtsepp/ubNIjPOsJ6k8jSqM="
+ "pname": "Microsoft.Build.Framework",
+ "version": "17.11.31",
+ "hash": "sha256-YS4oASrmC5dmZrx5JPS7SfKmUpIJErlUpVDsU3VrfFE="
},
{
"pname": "Microsoft.Build.Framework",
- "version": "17.14.28",
- "hash": "sha256-7RzEyIipumafwLW1xN1q23114NafG6PT0+RADElNsiM="
- },
- {
- "pname": "Microsoft.Build.Framework",
- "version": "17.7.2",
- "hash": "sha256-fNWmVQYFTJDveAGmxEdNqJRAczV6+Ep8RA8clKBJFqw="
- },
- {
- "pname": "Microsoft.Build.Tasks.Core",
- "version": "17.14.28",
- "hash": "sha256-M9zRXYijH2HtLlRXbrUK1a1LQ9zkT+DC9ZmMiiVZwv0="
- },
- {
- "pname": "Microsoft.Build.Tasks.Core",
- "version": "17.7.2",
- "hash": "sha256-OrV/qWgZHzGlNUmaSfX5wDBcmg1aQeF3/OUHpSH+uZU="
- },
- {
- "pname": "Microsoft.Build.Utilities.Core",
- "version": "17.14.28",
- "hash": "sha256-VFfO+UpyTpw2X/qiCCOCYzvMLuu7B+XVSSpJZQLkPzU="
- },
- {
- "pname": "Microsoft.Build.Utilities.Core",
- "version": "17.7.2",
- "hash": "sha256-oatF0KfuP1nb4+OLNKg2/R/ZLO4EiACaO5leaxMEY4A="
+ "version": "18.0.2",
+ "hash": "sha256-fO31KAdDs2J0RUYD1ov9UB3ucsbALan7K0YdWW+yg7A="
},
{
"pname": "Microsoft.CodeAnalysis.Analyzers",
@@ -46,28 +21,28 @@
},
{
"pname": "Microsoft.CodeAnalysis.Common",
- "version": "4.14.0",
- "hash": "sha256-ne/zxH3GqoGB4OemnE8oJElG5mai+/67ASaKqwmL2BE="
+ "version": "5.0.0",
+ "hash": "sha256-g4ALvBSNyHEmSb1l5TFtWW7zEkiRmhqLx4XWZu9sr2U="
},
{
"pname": "Microsoft.CodeAnalysis.CSharp",
- "version": "4.14.0",
- "hash": "sha256-5Mzj3XkYYLkwDWh17r1NEXSbXwwWYQPiOmkSMlgo1JY="
+ "version": "5.0.0",
+ "hash": "sha256-ctBCkQGFpH/xT5rRE3xibu9YxPD108RuC4a4Z25koG8="
},
{
"pname": "Microsoft.CodeAnalysis.CSharp.Workspaces",
- "version": "4.14.0",
- "hash": "sha256-aNbV1a0yYBs0fpQawG6LXcbyoE8en+YFSpV5vcYE4J4="
+ "version": "5.0.0",
+ "hash": "sha256-yWVcLt/f2CouOfFy966glGdtSFy+RcgrU1dd9UtlL/Q="
},
{
"pname": "Microsoft.CodeAnalysis.Workspaces.Common",
- "version": "4.14.0",
- "hash": "sha256-0YfeaJe01WBUm9avy4a8FacQJXA1NkpnDpiXu4yz88I="
+ "version": "5.0.0",
+ "hash": "sha256-Bir5e1gEhgQQ6upQmVKQHAKLRfenAu60DAzNupNnZsQ="
},
{
"pname": "Microsoft.CodeAnalysis.Workspaces.MSBuild",
- "version": "4.14.0",
- "hash": "sha256-5SJfpRqzqCK0UbkmAaJpA/r1XJb0YAriMMeQHYC4d+o="
+ "version": "5.0.0",
+ "hash": "sha256-+58+iqTayTiE0pDaog1U8mjaDA8bNNDLA8gjCQZZudo="
},
{
"pname": "Microsoft.EntityFrameworkCore",
@@ -91,8 +66,8 @@
},
{
"pname": "Microsoft.EntityFrameworkCore.Design",
- "version": "10.0.1",
- "hash": "sha256-GGNZIGNEMhSGaMRFkRN4bOuCUBs5YVnX8klXarm319U="
+ "version": "10.0.2",
+ "hash": "sha256-bTShsGux0y/49PIIMb/4ZX3x5+rPacvT5/NcooNCI1Y="
},
{
"pname": "Microsoft.EntityFrameworkCore.Relational",
@@ -101,24 +76,14 @@
},
{
"pname": "Microsoft.EntityFrameworkCore.Relational",
- "version": "10.0.1",
- "hash": "sha256-zLgxr/iW9HP8Fip1IDgr7X0Ar8OWKDvVmoEt65gG6VY="
+ "version": "10.0.2",
+ "hash": "sha256-Y4jPpoYhKizg5wF6QfkBX4sYlE2FU1bYhfoDN3xkhKM="
},
{
"pname": "Microsoft.Extensions.Caching.Abstractions",
"version": "10.0.2",
"hash": "sha256-nKmQuZTt1g5/8gBajo7wdCV64kdCucdiQR8JTt7ZZb0="
},
- {
- "pname": "Microsoft.Extensions.Caching.Memory",
- "version": "10.0.0",
- "hash": "sha256-AMgDSm1k6q0s17spGtyR5q8nAqUFDOxl/Fe38f9M+d4="
- },
- {
- "pname": "Microsoft.Extensions.Caching.Memory",
- "version": "10.0.1",
- "hash": "sha256-Qb7xK6VEZDas0lJFaW1suKdFjtkSYwLHHxkQEfWIU2A="
- },
{
"pname": "Microsoft.Extensions.Caching.Memory",
"version": "10.0.2",
@@ -129,16 +94,6 @@
"version": "10.0.2",
"hash": "sha256-dBJAKDyp/sm+ZSMQfH0+4OH8Jnv1s20aHlWS6HNnH+c="
},
- {
- "pname": "Microsoft.Extensions.Configuration.Abstractions",
- "version": "10.0.0",
- "hash": "sha256-GcgrnTAieCV7AVT13zyOjfwwL86e99iiO/MiMOxPGG0="
- },
- {
- "pname": "Microsoft.Extensions.Configuration.Abstractions",
- "version": "10.0.1",
- "hash": "sha256-s4PDp+vtzdxKIxnOT3+dDRoTDopyl8kqmmw4KDnkOtQ="
- },
{
"pname": "Microsoft.Extensions.Configuration.Abstractions",
"version": "10.0.2",
@@ -191,8 +146,8 @@
},
{
"pname": "Microsoft.Extensions.DependencyModel",
- "version": "10.0.1",
- "hash": "sha256-XIj2jEURe25YA4RhBSuCqQpic0YP+TZaO/dbBPCjad8="
+ "version": "10.0.2",
+ "hash": "sha256-w/dGIjtZiGH+KW3969BPOdQpQEV+WB7RPTa2MK2DavE="
},
{
"pname": "Microsoft.Extensions.Diagnostics",
@@ -229,16 +184,6 @@
"version": "10.0.2",
"hash": "sha256-mkeKUXepn4bfEdZFXdURmNEFdGiHQdpcxnm6joG+pUA="
},
- {
- "pname": "Microsoft.Extensions.Logging",
- "version": "10.0.0",
- "hash": "sha256-P+zPAadLL63k/GqK34/qChqQjY9aIRxZfxlB9lqsSrs="
- },
- {
- "pname": "Microsoft.Extensions.Logging",
- "version": "10.0.1",
- "hash": "sha256-zuLP3SIpCToMOlIPOEv3Kq8y/minecd8k8GSkxFo13E="
- },
{
"pname": "Microsoft.Extensions.Logging",
"version": "10.0.2",
@@ -304,11 +249,6 @@
"version": "10.0.2",
"hash": "sha256-WJahsWyT5wYdLPEJufHKpb3l/dl7D2iw2SnMK0Jr53U="
},
- {
- "pname": "Microsoft.Extensions.Primitives",
- "version": "10.0.1",
- "hash": "sha256-EXmukq09erT4s+miQpBSYy3IY4HxxKlwEPL43/KoyEc="
- },
{
"pname": "Microsoft.Extensions.Primitives",
"version": "10.0.2",
@@ -320,14 +260,9 @@
"hash": "sha256-ZNLusK1CRuq5BZYZMDqaz04PIKScE2Z7sS2tehU7EJs="
},
{
- "pname": "Microsoft.NET.StringTools",
- "version": "17.14.28",
- "hash": "sha256-UzREyvDxkiOQ4cEOQ5UCjkwXGrldIDCcbefECTPGjXI="
- },
- {
- "pname": "Microsoft.NET.StringTools",
- "version": "17.7.2",
- "hash": "sha256-hQE07TCgcQuyu9ZHVq2gPDb0+xe8ECJUdrgh17bJP4o="
+ "pname": "Microsoft.VisualStudio.SolutionPersistence",
+ "version": "1.0.52",
+ "hash": "sha256-KZGPtOXe6Hv8RrkcsgoLKTRyaCScIpQEa2NhNB3iOXw="
},
{
"pname": "Mono.TextTemplating",
@@ -354,16 +289,6 @@
"version": "6.0.0",
"hash": "sha256-uPetUFZyHfxjScu5x4agjk9pIhbCkt5rG4Axj25npcQ="
},
- {
- "pname": "System.CodeDom",
- "version": "7.0.0",
- "hash": "sha256-7IPt39cY+0j0ZcRr/J45xPtEjnSXdUJ/5ai3ebaYQiE="
- },
- {
- "pname": "System.CodeDom",
- "version": "9.0.0",
- "hash": "sha256-578lcBgswW0eM16r0EnJzfGodPx86RxxFoZHc2PSzsw="
- },
{
"pname": "System.Composition",
"version": "9.0.0",
@@ -394,84 +319,9 @@
"version": "9.0.0",
"hash": "sha256-F5fpTUs3Rr7yP/NyIzr+Xn5NdTXXp8rrjBnF9UBBUog="
},
- {
- "pname": "System.Configuration.ConfigurationManager",
- "version": "7.0.0",
- "hash": "sha256-SgBexTTjRn23uuXvkzO0mz0qOfA23MiS4Wv+qepMLZE="
- },
- {
- "pname": "System.Configuration.ConfigurationManager",
- "version": "9.0.0",
- "hash": "sha256-+pLnTC0YDP6Kjw5DVBiFrV/Q3x5is/+6N6vAtjvhVWk="
- },
{
"pname": "System.Diagnostics.EventLog",
"version": "10.0.2",
"hash": "sha256-rezZk0M4+MWRxwGSFzXfvekrhL8qLTp7Pc8YsSy/4nE="
- },
- {
- "pname": "System.Diagnostics.EventLog",
- "version": "9.0.0",
- "hash": "sha256-tPvt6yoAp56sK/fe+/ei8M65eavY2UUhRnbrREj/Ems="
- },
- {
- "pname": "System.Formats.Nrbf",
- "version": "9.0.0",
- "hash": "sha256-c4qf6CocQUZB0ySGQd8s15PXY7xfrjQqMGXxkwytKyw="
- },
- {
- "pname": "System.Reflection.MetadataLoadContext",
- "version": "7.0.0",
- "hash": "sha256-VYl6SFD130K9Aw4eJH16ApJ9Sau4Xu0dcxEip2veuTI="
- },
- {
- "pname": "System.Resources.Extensions",
- "version": "9.0.0",
- "hash": "sha256-y2gLEMuAy6QfEyNJxABC/ayMWGnwlpX735jsUQLktho="
- },
- {
- "pname": "System.Security.Cryptography.Pkcs",
- "version": "7.0.0",
- "hash": "sha256-3J3vL9hcKSuZjT2GKappa2A9p2xJm1nH2asTNAl8ZCA="
- },
- {
- "pname": "System.Security.Cryptography.Pkcs",
- "version": "7.0.2",
- "hash": "sha256-qS5Z/Yo8J+f3ExVX5Qkcpj1Z57oUZqz5rWa1h5bVpl8="
- },
- {
- "pname": "System.Security.Cryptography.Pkcs",
- "version": "9.0.0",
- "hash": "sha256-AjG14mGeSc2Ka4QSelGBM1LrGBW3VJX60lnihKyJjGY="
- },
- {
- "pname": "System.Security.Cryptography.ProtectedData",
- "version": "9.0.0",
- "hash": "sha256-gPgPU7k/InTqmXoRzQfUMEKL3QuTnOKowFqmXTnWaBQ="
- },
- {
- "pname": "System.Security.Cryptography.Xml",
- "version": "7.0.1",
- "hash": "sha256-CH8+JVC8LyCSW75/6ZQ7ecMbSOAE1c16z4dG8JTp01w="
- },
- {
- "pname": "System.Security.Cryptography.Xml",
- "version": "9.0.0",
- "hash": "sha256-SQJWwAFrJUddEU6JiZB52FM9tGjRlJAYH8oYVzG5IJU="
- },
- {
- "pname": "System.Security.Permissions",
- "version": "7.0.0",
- "hash": "sha256-DOFoX+AKRmrkllykHheR8FfUXYx/Ph+I/HYuReQydXI="
- },
- {
- "pname": "System.Security.Permissions",
- "version": "9.0.0",
- "hash": "sha256-BFrA9ottmQtLIAiKiGRbfSUpzNJwuaOCeFRDN4Z0ku0="
- },
- {
- "pname": "System.Windows.Extensions",
- "version": "9.0.0",
- "hash": "sha256-RErD+Ju15qtnwdwB7E0SjjJGAnhXwJyC7UPcl24Z3Vs="
}
]
diff --git a/extra/admin-api/nuget.config b/extra/admin-api/nuget.config
index aa87aca25..3cea9dd15 100644
--- a/extra/admin-api/nuget.config
+++ b/extra/admin-api/nuget.config
@@ -2,8 +2,8 @@
-
-
+
+
diff --git a/extra/admin-api/outputs.nix b/extra/admin-api/outputs.nix
index 20ae962d0..8392629a6 100644
--- a/extra/admin-api/outputs.nix
+++ b/extra/admin-api/outputs.nix
@@ -65,6 +65,11 @@ flake-utils.lib.eachSystem flake-utils.lib.allSystems (
in
{
# Interop
+ Spacebar-Interop-Cdn-Abstractions = makeNupkg {
+ name = "Spacebar.Interop.Cdn.Abstractions";
+ projectFile = "Interop/Spacebar.Interop.Cdn.Abstractions/Spacebar.Interop.Cdn.Abstractions.csproj";
+ nugetDeps = Interop/Spacebar.Interop.Cdn.Abstractions/deps.json;
+ };
Spacebar-Interop-Replication-Abstractions = makeNupkg {
name = "Spacebar.Interop.Replication.Abstractions";
projectFile = "Interop/Spacebar.Interop.Replication.Abstractions/Spacebar.Interop.Replication.Abstractions.csproj";
@@ -105,6 +110,13 @@ flake-utils.lib.eachSystem flake-utils.lib.allSystems (
packNupkg = false;
projectReferences = [ proj.Spacebar-Models-Db ];
};
+ Spacebar-Cdn-Fsck = makeNupkg {
+ name = "Spacebar.Cdn.Fsck";
+ projectFile = "Utilities/Spacebar.Cdn.Fsck/Spacebar.Cdn.Fsck.csproj";
+ nugetDeps = Utilities/Spacebar.Cdn.Fsck/deps.json;
+ packNupkg = false;
+ projectReferences = [ proj.Spacebar-Models-Db proj.Spacebar-Interop-Cdn-Abstractions ];
+ };
# Main projects
Spacebar-AdminApi = makeNupkg {
diff --git a/nix/modules/default/cdn-cs.nix b/nix/modules/default/cdn-cs.nix
new file mode 100644
index 000000000..b123e245d
--- /dev/null
+++ b/nix/modules/default/cdn-cs.nix
@@ -0,0 +1,333 @@
+self:
+{
+ config,
+ lib,
+ pkgs,
+ spacebar,
+ ...
+}:
+
+let
+ cfg = config.services.spacebarchat-server;
+ jsonFormat = pkgs.formats.json { };
+ configFile =
+ let
+ endpointSettings = {
+ api = {
+ endpointPublic = "http${if cfg.apiEndpoint.useSsl then "s" else ""}://${cfg.apiEndpoint.host}:${toString cfg.apiEndpoint.publicPort}";
+ };
+ cdn = {
+ endpointPublic = "http${if cfg.cdnEndpoint.useSsl then "s" else ""}://${cfg.cdnEndpoint.host}:${toString cfg.cdnEndpoint.publicPort}";
+ endpointPrivate = "http://127.0.0.1:${toString cfg.cdnEndpoint.localPort}";
+ };
+ gateway = {
+ endpointPublic = "ws${if cfg.gatewayEndpoint.useSsl then "s" else ""}://${cfg.gatewayEndpoint.host}:${toString cfg.gatewayEndpoint.publicPort}";
+ };
+ general = {
+ serverName = cfg.serverName;
+ };
+ }
+ // (
+ if cfg.enableAdmInApi then
+ {
+ adminApi = {
+ endpointPublic = "http${if cfg.adminApiEndpoint.useSsl then "s" else ""}://${cfg.adminApiEndpoint.host}:${toString cfg.adminApiEndpoint.publicPort}";
+ };
+ }
+ else
+ { }
+ );
+ in
+ jsonFormat.generate "spacebarchat-server.json" (lib.recursiveUpdate endpointSettings cfg.settings);
+in
+{
+ imports = [
+ ./integration-nginx.nix
+ ./secrets.nix
+ ./users.nix
+ ];
+ options.services.spacebarchat-server =
+ let
+ mkEndpointOptions = import ./options-subtypes/mkEndpointOptions.nix { inherit lib; };
+ in
+ {
+ enable = lib.mkEnableOption "Spacebar server";
+ enableAdminApi = lib.mkEnableOption "Spacebar server Admin API";
+ enableCdnCs = lib.mkEnableOption "Spacebar's experimental CDN rewrite";
+ package = lib.mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} "spacebar-server" { default = "default"; };
+ databaseFile = lib.mkOption {
+ type = lib.types.nullOr lib.types.path;
+ default = null;
+ description = ''
+ Path to a file containing a definition of the `DATABASE` environment variable database connection string.
+ Example content: `DATABASE=postgres://username:password@host-IP:port/databaseName`.
+ See https://docs.spacebar.chat/setup/server/database/.
+ '';
+ };
+
+ serverName = lib.mkOption {
+ type = lib.types.str;
+ description = "The server name for this Spacebar instance (aka. common name, usually the domain where your well known is hosted).";
+ };
+ adminApiEndpoint = mkEndpointOptions "admin-api.sb.localhost" 3004;
+ apiEndpoint = mkEndpointOptions "api.sb.localhost" 3001;
+ gatewayEndpoint = mkEndpointOptions "gateway.sb.localhost" 3003;
+ cdnEndpoint = mkEndpointOptions "cdn.sb.localhost" 3003;
+ cdnPath = lib.mkOption {
+ type = lib.types.str;
+ default = "./files";
+ description = "Path to store CDN files.";
+ };
+
+ extraEnvironment = lib.mkOption {
+ default = { };
+ description = ''
+ Environment variables passed to spacebarchat-server.
+ See https://docs.spacebar.chat/setup/server/configuration/env for supported values.
+ '';
+ type = lib.types.submodule {
+ freeformType =
+ with lib.types;
+ attrsOf (oneOf [
+ str
+ bool
+ int
+ ]);
+ options = {
+ THREADS = lib.mkOption {
+ type = lib.types.ints.positive;
+ default = 1;
+ description = "Number of threads to run Spacebar on when using bundle. Make sure you've enabled RabbitMQ if using more than one.";
+ };
+ };
+ };
+ };
+
+ settings = lib.mkOption {
+ type = jsonFormat.type;
+ default = { };
+ description = ''
+ Configuration for spacebarchat-server.
+ See https://docs.spacebar.chat/setup/server/configuration for supported values.
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable (
+ let
+ makeServerTsService = (
+ conf:
+ lib.recursiveUpdate
+ (lib.recursiveUpdate {
+ documentation = [ "https://docs.spacebar.chat/" ];
+ wantedBy = [ "multi-user.target" ];
+ wants = [ "network-online.target" ];
+ after = [ "network-online.target" ];
+ environment =
+ { }
+ // (if cfg.cdnSignaturePath != null then { CDN_SIGNATURE_PATH = "%d/cdnSignature"; } else { })
+ // (if cfg.legacyJwtSecretPath != null then { LEGACY_JWT_SECRET_PATH = "%d/legacyJwtSecret"; } else { })
+ // (if cfg.mailjetApiKeyPath != null then { MAILJET_API_KEY_PATH = "%d/mailjetApiKey"; } else { })
+ // (if cfg.mailjetApiSecretPath != null then { MAILJET_API_SECRET_PATH = "%d/mailjetApiSecret"; } else { })
+ // (if cfg.smtpPasswordPath != null then { SMTP_PASSWORD_PATH = "%d/smtpPassword"; } else { })
+ // (if cfg.gifApiKeyPath != null then { GIF_API_KEY_PATH = "%d/gifApiKey"; } else { })
+ // (if cfg.rabbitmqHostPath != null then { RABBITMQ_HOST_PATH = "%d/rabbitmqHost"; } else { })
+ // (if cfg.abuseIpDbApiKeyPath != null then { ABUSE_IP_DB_API_KEY_PATH = "%d/abuseIpDbApiKey"; } else { })
+ // (if cfg.captchaSecretKeyPath != null then { CAPTCHA_SECRET_KEY_PATH = "%d/captchaSecretKey"; } else { })
+ // (if cfg.captchaSiteKeyPath != null then { CAPTCHA_SITE_KEY_PATH = "%d/captchaSiteKey"; } else { })
+ // (if cfg.ipdataApiKeyPath != null then { IPDATA_API_KEY_PATH = "%d/ipdataApiKey"; } else { })
+ // (if cfg.requestSignaturePath != null then { REQUEST_SIGNATURE_PATH = "%d/requestSignature"; } else { });
+ serviceConfig = {
+ LoadCredential =
+ [ ]
+ ++ (if cfg.cdnSignaturePath != null then [ "cdnSignature:${cfg.cdnSignaturePath}" ] else [ ])
+ ++ (if cfg.legacyJwtSecretPath != null then [ "legacyJwtSecret:${cfg.legacyJwtSecretPath}" ] else [ ])
+ ++ (if cfg.mailjetApiKeyPath != null then [ "mailjetApiKey:${cfg.mailjetApiKeyPath}" ] else [ ])
+ ++ (if cfg.mailjetApiSecretPath != null then [ "mailjetApiSecret:${cfg.mailjetApiSecretPath}" ] else [ ])
+ ++ (if cfg.smtpPasswordPath != null then [ "smtpPassword:${cfg.smtpPasswordPath}" ] else [ ])
+ ++ (if cfg.gifApiKeyPath != null then [ "gifApiKey:${cfg.gifApiKeyPath}" ] else [ ])
+ ++ (if cfg.rabbitmqHostPath != null then [ "rabbitmqHost:${cfg.rabbitmqHostPath}" ] else [ ])
+ ++ (if cfg.abuseIpDbApiKeyPath != null then [ "abuseIpDbApiKey:${cfg.abuseIpDbApiKeyPath}" ] else [ ])
+ ++ (if cfg.captchaSecretKeyPath != null then [ "captchaSecretKey:${cfg.captchaSecretKeyPath}" ] else [ ])
+ ++ (if cfg.captchaSiteKeyPath != null then [ "captchaSiteKey:${cfg.captchaSiteKeyPath}" ] else [ ])
+ ++ (if cfg.ipdataApiKeyPath != null then [ "ipdataApiKey:${cfg.ipdataApiKeyPath}" ] else [ ])
+ ++ (if cfg.requestSignaturePath != null then [ "requestSignature:${cfg.requestSignaturePath}" ] else [ ]);
+
+ User = "spacebarchat";
+ Group = "spacebarchat";
+ DynamicUser = false;
+
+ LockPersonality = true;
+ NoNewPrivileges = true;
+
+ ProtectClock = true;
+ ProtectControlGroups = true;
+ ProtectHostname = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ PrivateDevices = true;
+ PrivateMounts = true;
+ PrivateUsers = true;
+ RestrictAddressFamilies = [
+ "AF_INET"
+ "AF_INET6"
+ "AF_UNIX"
+ ];
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ SystemCallArchitectures = "native";
+ SystemCallFilter = [
+ "@system-service"
+ "~@privileged"
+ "@chown" # Required for copying files with FICLONE, apparently.
+ ];
+ CapabilityBoundingSet = [
+ "~CAP_SYS_ADMIN"
+ "~CAP_AUDIT_*"
+ "~CAP_NET_(BIND_SERVICE|BROADCAST|RAW)"
+ "~CAP_NET_ADMIN" # No use for this as we don't currently use iptables for enforcing instance bans
+ "~CAP_SYS_TIME"
+ "~CAP_KILL"
+ "~CAP_(DAC_*|FOWNER|IPC_OWNER)"
+ "~CAP_LINUX_IMMUTABLE"
+ "~CAP_IPC_LOCK"
+ "~CAP_BPF"
+ "~CAP_SYS_TTY_CONFIG"
+ "~CAP_SYS_BOOT"
+ "~CAP_SYS_CHROOT"
+ "~CAP_BLOCK_SUSPEND"
+ "~CAP_LEASE"
+ "~CAP_(CHOWN|FSETID|FSETFCAP)" # Check if we need CAP_CHOWN for `fchown()` (FICLONE)?
+ "~CAP_SET(UID|GID|PCAP)"
+ "~CAP_MAC_*"
+ "~CAP_SYS_PTRACE"
+ "~CAP_SYS_(NICE|RESOURCE)"
+ "~CAP_SYS_RAWIO"
+ "~CAP_SYSLOG"
+ ];
+ RestrictSUIDSGID = true;
+
+ WorkingDirectory = "/var/lib/spacebar";
+ StateDirectory = "spacebar";
+ StateDirectoryMode = "0750";
+ RuntimeDirectory = "spacebar";
+ RuntimeDirectoryMode = "0750";
+ ReadWritePaths = [ cfg.cdnPath ];
+ NoExecPaths = [ cfg.cdnPath ];
+
+ Restart = "on-failure";
+ RestartSec = 10;
+ StartLimitBurst = 5;
+ UMask = "077";
+ }
+ // lib.optionalAttrs (cfg.databaseFile != null) { EnvironmentFile = cfg.databaseFile; };
+ } conf)
+ {
+ }
+ );
+ in
+ {
+ assertions = [
+ # {
+ # assertion = lib.all (map (key: !(key == "CONFIG_PATH" || key == "CONFIG_READONLY" || key == "PORT" || key == "STORAGE_LOCATION")) (lib.attrNames cfg.extraEnvironment));
+ # message = "You cannot set CONFIG_PATH, CONFIG_READONLY, PORT or STORAGE_LOCATION in extraEnvironment, these are managed by the NixOS module.";
+ # }
+ ];
+
+ systemd.services.spacebar-apply-migrations = makeServerTsService {
+ description = "Spacebar Server - Apply DB migrations";
+ # after = lib.optional config.services.postgresql.enable "postgresql.service";
+ # requires = lib.optional config.services.postgresql.enable "postgresql.service";
+ environment = builtins.mapAttrs (_: val: builtins.toString val) (
+ cfg.extraEnvironment
+ // {
+ # things we force...
+ CONFIG_PATH = configFile;
+ CONFIG_READONLY = 1;
+ }
+ );
+ serviceConfig = {
+ ExecStart = "${cfg.package}/bin/apply-migrations";
+ Type = "oneshot";
+ RemainAfterExit = true;
+ TimeoutStartSec = 15;
+ RestartSec = 1;
+ StartLimitBurst = 15;
+ };
+ };
+
+ systemd.services.spacebar-api = makeServerTsService {
+ description = "Spacebar Server - API";
+ after = [ "spacebar-apply-migrations.service" ];
+ requires = [ "spacebar-apply-migrations.service" ];
+ environment = builtins.mapAttrs (_: val: builtins.toString val) (
+ {
+ # things we set by default...
+ EVENT_TRANSMISSION = "unix";
+ EVENT_SOCKET_PATH = "/run/spacebar/";
+ }
+ // cfg.extraEnvironment
+ // {
+ # things we force...
+ CONFIG_PATH = configFile;
+ CONFIG_READONLY = 1;
+ PORT = toString cfg.apiEndpoint.localPort;
+ STORAGE_LOCATION = cfg.cdnPath;
+ }
+ );
+ serviceConfig = {
+ ExecStart = "${cfg.package}/bin/start-api";
+ };
+ };
+
+ systemd.services.spacebar-gateway = makeServerTsService {
+ description = "Spacebar Server - Gateway";
+ after = [ "spacebar-apply-migrations.service" ];
+ requires = [ "spacebar-apply-migrations.service" ];
+ environment = builtins.mapAttrs (_: val: builtins.toString val) (
+ {
+ # things we set by default...
+ EVENT_TRANSMISSION = "unix";
+ EVENT_SOCKET_PATH = "/run/spacebar/";
+ }
+ // cfg.extraEnvironment
+ // {
+ # things we force...
+ CONFIG_PATH = configFile;
+ CONFIG_READONLY = 1;
+ PORT = toString cfg.gatewayEndpoint.localPort;
+ STORAGE_LOCATION = cfg.cdnPath;
+ }
+ );
+ serviceConfig = {
+ ExecStart = "${cfg.package}/bin/start-gateway";
+ };
+ };
+
+ systemd.services.spacebar-cdn = lib.mkIf (!cfg.useCdnCs) (makeServerTsService {
+ description = "Spacebar Server - CDN";
+ after = [ "spacebar-apply-migrations.service" ];
+ requires = [ "spacebar-apply-migrations.service" ];
+ environment = builtins.mapAttrs (_: val: builtins.toString val) (
+ {
+ # things we set by default...
+ EVENT_TRANSMISSION = "unix";
+ EVENT_SOCKET_PATH = "/run/spacebar/";
+ }
+ // cfg.extraEnvironment
+ // {
+ # things we force...
+ CONFIG_PATH = configFile;
+ CONFIG_READONLY = 1;
+ PORT = toString cfg.cdnEndpoint.localPort;
+ STORAGE_LOCATION = cfg.cdnPath;
+ }
+ );
+ serviceConfig = {
+ ExecStart = "${cfg.package}/bin/start-cdn";
+ };
+ });
+ }
+ );
+}
diff --git a/nix/modules/default/default.nix b/nix/modules/default/default.nix
index 6bd1045a3..db022e407 100644
--- a/nix/modules/default/default.nix
+++ b/nix/modules/default/default.nix
@@ -10,26 +10,39 @@ self:
let
cfg = config.services.spacebarchat-server;
jsonFormat = pkgs.formats.json { };
- configFile = jsonFormat.generate "spacebarchat-server.json" (
- lib.recursiveUpdate {
- api = {
- endpointPublic = "http${if cfg.apiEndpoint.useSsl then "s" else ""}://${cfg.apiEndpoint.host}:${toString cfg.apiEndpoint.publicPort}";
- };
- cdn = {
- endpointPublic = "http${if cfg.cdnEndpoint.useSsl then "s" else ""}://${cfg.cdnEndpoint.host}:${toString cfg.cdnEndpoint.publicPort}";
- endpointPrivate = "http://127.0.0.1:${toString cfg.cdnEndpoint.localPort}";
- };
- gateway = {
- endpointPublic = "ws${if cfg.gatewayEndpoint.useSsl then "s" else ""}://${cfg.gatewayEndpoint.host}:${toString cfg.gatewayEndpoint.publicPort}";
- };
- general = {
- serverName = cfg.serverName;
- };
- } cfg.settings
- );
+ configFile =
+ let
+ endpointSettings = {
+ api = {
+ endpointPublic = "http${if cfg.apiEndpoint.useSsl then "s" else ""}://${cfg.apiEndpoint.host}:${toString cfg.apiEndpoint.publicPort}";
+ };
+ cdn = {
+ endpointPublic = "http${if cfg.cdnEndpoint.useSsl then "s" else ""}://${cfg.cdnEndpoint.host}:${toString cfg.cdnEndpoint.publicPort}";
+ endpointPrivate = "http://127.0.0.1:${toString cfg.cdnEndpoint.localPort}";
+ };
+ gateway = {
+ endpointPublic = "ws${if cfg.gatewayEndpoint.useSsl then "s" else ""}://${cfg.gatewayEndpoint.host}:${toString cfg.gatewayEndpoint.publicPort}";
+ };
+ general = {
+ serverName = cfg.serverName;
+ };
+ }
+ // (
+ if cfg.enableAdmInApi then
+ {
+ adminApi = {
+ endpointPublic = "http${if cfg.adminApiEndpoint.useSsl then "s" else ""}://${cfg.adminApiEndpoint.host}:${toString cfg.adminApiEndpoint.publicPort}";
+ };
+ }
+ else
+ { }
+ );
+ in
+ jsonFormat.generate "spacebarchat-server.json" (lib.recursiveUpdate endpointSettings cfg.settings);
in
{
imports = [
+ ./cdn-cs.nix
./integration-nginx.nix
./secrets.nix
./users.nix
@@ -41,6 +54,7 @@ in
{
enable = lib.mkEnableOption "Spacebar server";
enableAdminApi = lib.mkEnableOption "Spacebar server Admin API";
+ enableCdnCs = lib.mkEnableOption "Spacebar's experimental CDN rewrite";
package = lib.mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} "spacebar-server" { default = "default"; };
databaseFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
@@ -56,6 +70,7 @@ in
type = lib.types.str;
description = "The server name for this Spacebar instance (aka. common name, usually the domain where your well known is hosted).";
};
+ adminApiEndpoint = mkEndpointOptions "admin-api.sb.localhost" 3004;
apiEndpoint = mkEndpointOptions "api.sb.localhost" 3001;
gatewayEndpoint = mkEndpointOptions "gateway.sb.localhost" 3003;
cdnEndpoint = mkEndpointOptions "cdn.sb.localhost" 3003;
@@ -223,8 +238,8 @@ in
systemd.services.spacebar-apply-migrations = makeServerTsService {
description = "Spacebar Server - Apply DB migrations";
-# after = lib.optional config.services.postgresql.enable "postgresql.service";
-# requires = lib.optional config.services.postgresql.enable "postgresql.service";
+ # after = lib.optional config.services.postgresql.enable "postgresql.service";
+ # requires = lib.optional config.services.postgresql.enable "postgresql.service";
environment = builtins.mapAttrs (_: val: builtins.toString val) (
cfg.extraEnvironment
// {
@@ -291,7 +306,7 @@ in
};
};
- systemd.services.spacebar-cdn = makeServerTsService {
+ systemd.services.spacebar-cdn = lib.mkIf (!cfg.useCdnCs) (makeServerTsService {
description = "Spacebar Server - CDN";
after = [ "spacebar-apply-migrations.service" ];
requires = [ "spacebar-apply-migrations.service" ];
@@ -313,7 +328,7 @@ in
serviceConfig = {
ExecStart = "${cfg.package}/bin/start-cdn";
};
- };
+ });
}
);
}