nix/cdn work

This commit is contained in:
Rory&
2026-02-28 19:49:44 +01:00
parent 0784add134
commit 829ada2607
17 changed files with 408 additions and 175 deletions
+62 -43
View File
@@ -17,6 +17,13 @@
<entry key="_/home/Rory/git/spacebar/server-master" value="2oJ9u2nkEFq1qQW1NFF69ECjiYu" />
</persistenceIdMap>
</component>
<component name="CopilotUserSelectedModel">
<selectedModels>
<entry key="edit-panel" value="GPT-5 mini" />
<entry key="chat-panel" value="Gemini 3 Pro (Preview)" />
<entry key="agent-panel" value="Gemini 3 Pro (Preview)" />
</selectedModels>
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
@@ -43,6 +50,9 @@
<setting file="file://$PROJECT_DIR$/node_modules/typeorm/query-builder/WhereExpressionBuilder.d.ts" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/node_modules/typeorm/repository/BaseEntity.d.ts" root0="SKIP_INSPECTION" />
</component>
<component name="NamedScopeManager">
<scope name="src" pattern="!file[server]:*/||file[server]:src//*" />
</component>
<component name="ProblemsViewState">
<option name="selectedTabId" value="ProjectErrors" />
</component>
@@ -55,52 +65,52 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"NIXITCH_NIXPKGS_CONFIG": "/etc/nix/nixpkgs-config.nix",
"NIXITCH_NIX_CONF_DIR": "",
"NIXITCH_NIX_OTHER_STORES": "",
"NIXITCH_NIX_PATH": "/home/Rory/.nix-defexpr/channels:nixpkgs=/nix/store/wb6agba4kfsxpbnb5hzlq58vkjzvbsk6-source",
"NIXITCH_NIX_PROFILES": "/run/current-system/sw /nix/var/nix/profiles/default /etc/profiles/per-user/Rory /home/Rory/.local/state/nix/profile /nix/profile /home/Rory/.nix-profile",
"NIXITCH_NIX_REMOTE": "",
"NIXITCH_NIX_USER_PROFILE_DIR": "/nix/var/nix/profiles/per-user/Rory",
"Node.js.Server.ts.executor": "Debug",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"git-widget-placeholder": "master",
"javascript.nodejs.core.library.configured.version": "24.11.1",
"javascript.nodejs.core.library.typings.version": "24.10.9",
"last_opened_file_path": "/home/Rory/git/spacebar/server-master/nix/modules/default",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.standard": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.standard": "",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_interpreter_path": "node",
"nodejs_package_manager_path": "npm",
"npm.Start API.executor": "Run",
"npm.Start CDN.executor": "Run",
"npm.Start Gateway.executor": "Run",
"npm.build.executor": "Run",
"npm.build:src.executor": "Run",
"npm.build:src:tsgo.executor": "Run",
"npm.build:tsgo.executor": "Run",
"npm.start.executor": "Debug",
"prettierjs.PrettierConfiguration.Package": "/home/Rory/git/spacebar/server-master/node_modules/prettier",
"ts.external.directory.path": "/home/Rory/git/spacebar/server-master/node_modules/typescript/lib"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;NIXITCH_NIXPKGS_CONFIG&quot;: &quot;/etc/nix/nixpkgs-config.nix&quot;,
&quot;NIXITCH_NIX_CONF_DIR&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_OTHER_STORES&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_PATH&quot;: &quot;/home/Rory/.nix-defexpr/channels:nixpkgs=/nix/store/wb6agba4kfsxpbnb5hzlq58vkjzvbsk6-source&quot;,
&quot;NIXITCH_NIX_PROFILES&quot;: &quot;/run/current-system/sw /nix/var/nix/profiles/default /etc/profiles/per-user/Rory /home/Rory/.local/state/nix/profile /nix/profile /home/Rory/.nix-profile&quot;,
&quot;NIXITCH_NIX_REMOTE&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_USER_PROFILE_DIR&quot;: &quot;/nix/var/nix/profiles/per-user/Rory&quot;,
&quot;Node.js.Server.ts.executor&quot;: &quot;Debug&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;javascript.nodejs.core.library.configured.version&quot;: &quot;24.11.1&quot;,
&quot;javascript.nodejs.core.library.typings.version&quot;: &quot;24.10.9&quot;,
&quot;last_opened_file_path&quot;: &quot;/home/Rory/git/spacebar/server-master/nix/modules/default&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.standard&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.standard&quot;: &quot;&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_interpreter_path&quot;: &quot;node&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;npm.Start API.executor&quot;: &quot;Run&quot;,
&quot;npm.Start CDN.executor&quot;: &quot;Run&quot;,
&quot;npm.Start Gateway.executor&quot;: &quot;Run&quot;,
&quot;npm.build.executor&quot;: &quot;Run&quot;,
&quot;npm.build:src.executor&quot;: &quot;Run&quot;,
&quot;npm.build:src:tsgo.executor&quot;: &quot;Run&quot;,
&quot;npm.build:tsgo.executor&quot;: &quot;Run&quot;,
&quot;npm.start.executor&quot;: &quot;Debug&quot;,
&quot;prettierjs.PrettierConfiguration.Package&quot;: &quot;/home/Rory/git/spacebar/server-master/node_modules/prettier&quot;,
&quot;ts.external.directory.path&quot;: &quot;/home/Rory/git/spacebar/server-master/node_modules/typescript/lib&quot;
},
"keyToStringList": {
"DatabaseDriversLRU": [
"postgresql"
&quot;keyToStringList&quot;: {
&quot;DatabaseDriversLRU&quot;: [
&quot;postgresql&quot;
],
"GitStage.ChangesTree.GroupingKeys": [
"directory",
"module",
"repository"
&quot;GitStage.ChangesTree.GroupingKeys&quot;: [
&quot;directory&quot;,
&quot;module&quot;,
&quot;repository&quot;
]
}
}]]></component>
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/nix/modules/default" />
@@ -191,7 +201,8 @@
<workItem from="1769278836997" duration="449000" />
<workItem from="1771558270982" duration="82802000" />
<workItem from="1771818832744" duration="44139000" />
<workItem from="1771996450006" duration="61321000" />
<workItem from="1771996450006" duration="81615000" />
<workItem from="1772272118101" duration="20496000" />
</task>
<servers />
</component>
@@ -230,4 +241,12 @@
</breakpoints>
</breakpoint-manager>
</component>
<component name="github-copilot-workspace">
<instructionFileLocations>
<option value=".github/instructions" />
</instructionFileLocations>
<promptFileLocations>
<option value=".github/prompts" />
</promptFileLocations>
</component>
</project>
@@ -7,25 +7,17 @@ using Spacebar.Interop.Cdn.Abstractions;
namespace Spacebar.Cdn.Controllers;
[ApiController]
public class GetImageController(LruFileCache lfc, IFileSource fs, DiscordImageResizeService dirs) : ControllerBase {
[HttpGet("/avatars/{_:required}")]
public class GetImageController(LruFileCache lfc, IFileSource fs, DiscordImageResizeService dirs) : ImageController {
// [HttpGet("/avatars/{_:required}")]
[HttpGet("/emojis/{emoji_id:required}.{ext:required}")]
[HttpGet("/stickers/{sticker_id:required}.{ext:required}")]
[HttpGet("/avatars/{user_id:required}/{avatar_hash:required}.{ext:required}")]
// [HttpGet("/avatars/{user_id:required}/{avatar_hash:required}.{ext:required}")]
[HttpGet("/banners/{user_id:required}/{user_banner:required}.{ext:required}")]
public async Task<IActionResult> GetImage(string? ext) {
var originalKey = fs.BaseUrl + Request.Path;
var cacheKey = Request.Path + Request.QueryString;
DiscordImageResizeParams resizeParams = new() {
Size = Request.Query.ContainsKey("size") && uint.TryParse(Request.Query["size"], out uint size) ? size : null,
Quality = Request.Query.ContainsKey("quality") && Enum.TryParse<DiscordImageResizeQuality>(Request.Query["quality"], true, out var quality) ? quality : DiscordImageResizeQuality.High,
KeepAspectRatio = !Request.Query.ContainsKey("keepAspectRatio") || !bool.TryParse(Request.Query["keepAspectRatio"], out bool kar) || kar,
Passthrough = Request.Query.ContainsKey("passthrough") && bool.TryParse(Request.Query["passthrough"], out bool pt) && pt,
Animated = Request.Query.ContainsKey("animated") && bool.TryParse(Request.Query["animated"], out bool an) && an,
SpacebarAllowUpscale = Request.Query.ContainsKey("allowUpscale") && bool.TryParse(Request.Query["allowUpscale"], out bool au) && au,
SpacebarOptimiseGif = Request.Query.ContainsKey("optimiseGif") && bool.TryParse(Request.Query["optimiseGif"], out bool og) && og
};
DiscordImageResizeParams resizeParams = GetResizeParams();
var entry = await lfc.GetOrAdd(cacheKey, async () => {
var original = await fs.GetFile(Request.Path);
@@ -0,0 +1,35 @@
using System.Collections.Immutable;
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.Cdn.Controllers;
[ApiController]
public class StaticAssetController(LruFileCache lfc, IFileSource fs, DiscordImageResizeService dirs) : ImageController {
private static readonly Dictionary<string, string> defaultAvatarHashMap = new() {
{ "0", "4a8562cf00887030c416d3ec2d46385a" },
{ "1", "9b0bb198936784c45c72833cc426cc55" },
{ "2", "22341bdb500c7b63a93bbce957d1601e" },
{ "3", "d9977836b82058bf2f74eebd50edc095" },
{ "4", "9d6ddb4e4d899a533a8cc617011351c9" },
{ "5", "7213ab6677377974697dfdfbaf5f6a6f" },
};
private static readonly Dictionary<string, string> defaultGroupDMAvatarHashMap = new() {
{ "0", "3b70bb66089c60f8be5e214bf8574c9d" },
{ "1", "9581acd31832465bdeaa5385b0e919a3" },
{ "2", "a8a4727cf2dc2939bd3c657fad4463fa" },
{ "3", "2e46fe14586f8e95471c0917f56726b5" },
{ "4", "fac7e78de9753d4a37083bba74c1d9ef" },
{ "5", "4ab900144b0865430dc9be825c838faa" },
{ "6", "1276374a404452756f3c9cc2601508a5" },
{ "7", "904bf9f1b61f53ef4a3b7a893afeabe3" },
};
// [HttpGet("/embed/avatars/{userIndex}")]
// public async Task<IActionResult> GetDefaultUserAvatar(string userIndex) {
//
// }
}
@@ -0,0 +1,77 @@
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.Cdn.Controllers;
[ApiController]
public class UserController(LruFileCache lfc, IFileSource fs, DiscordImageResizeService dirs) : ImageController {
[HttpGet("/avatars/{userId}/{hash}.{ext}")]
public async Task<IActionResult> GetUserAvatar(string userId, string hash, string ext) {
var originalKey = fs.BaseUrl + Request.Path;
var cacheKey = Request.Path + Request.QueryString;
DiscordImageResizeParams resizeParams = GetResizeParams();
var entry = await lfc.GetOrAdd(cacheKey, async () => {
var original = await fs.GetFile(Request.Path);
if (Request.Query.Any()) {
using var img = await original.ToMagickImageCollectionAsync();
dirs.Apply(img, resizeParams);
var outStream = new MemoryStream();
await img.WriteAsync(outStream, img.First().Format);
outStream.Position = 0;
return new LruFileCache.Entry() {
Data = outStream.ReadToEnd().ToArray(),
MimeType = original.MimeType
};
}
return new LruFileCache.Entry() {
Data = original.Stream.ReadToEnd().ToArray(),
MimeType = original.MimeType
};
});
// byte array with mime type result
return new FileContentResult(entry.Data, entry.MimeType);
}
[HttpGet("/banners/{userId}/{hash}.{ext}")]
public async Task<IActionResult> GetUserBanner(string userId, string hash, string ext) {
var originalKey = fs.BaseUrl + Request.Path;
var cacheKey = Request.Path + Request.QueryString;
DiscordImageResizeParams resizeParams = GetResizeParams();
var entry = await lfc.GetOrAdd(cacheKey, async () => {
var original = await fs.GetFile(Request.Path);
if (Request.Query.Any()) {
using var img = await original.ToMagickImageCollectionAsync();
dirs.Apply(img, resizeParams);
var outStream = new MemoryStream();
await img.WriteAsync(outStream, img.First().Format);
outStream.Position = 0;
return new LruFileCache.Entry() {
Data = outStream.ReadToEnd().ToArray(),
MimeType = original.MimeType
};
}
return new LruFileCache.Entry() {
Data = original.Stream.ReadToEnd().ToArray(),
MimeType = original.MimeType
};
});
// byte array with mime type result
return new FileContentResult(entry.Data, entry.MimeType);
}
}
@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Mvc;
using Spacebar.AdminApi.TestClient.Services.Services;
namespace Spacebar.Cdn.Extensions;
public class ImageController : ControllerBase {
protected DiscordImageResizeParams GetResizeParams() {
return new() {
Size = Request.Query.ContainsKey("size") && uint.TryParse(Request.Query["size"], out uint size) ? size : null,
Quality = Request.Query.ContainsKey("quality") && Enum.TryParse<DiscordImageResizeQuality>(Request.Query["quality"], true, out var quality)
? quality
: DiscordImageResizeQuality.High,
KeepAspectRatio = !Request.Query.ContainsKey("keepAspectRatio") || !bool.TryParse(Request.Query["keepAspectRatio"], out bool kar) || kar,
Passthrough = Request.Query.ContainsKey("passthrough") && bool.TryParse(Request.Query["passthrough"], out bool pt) && pt,
Animated = Request.Query.ContainsKey("animated") && bool.TryParse(Request.Query["animated"], out bool an) && an,
SpacebarAllowUpscale = Request.Query.ContainsKey("allowUpscale") && bool.TryParse(Request.Query["allowUpscale"], out bool au) && au,
SpacebarOptimiseGif = Request.Query.ContainsKey("optimiseGif") && bool.TryParse(Request.Query["optimiseGif"], out bool og) && og
};
}
protected void SetSuccessCacheHeader() {
int cacheDuration = (int)TimeSpan.FromHours(6).TotalSeconds;
Response.Headers.CacheControl = $"public, max-age={cacheDuration}, s-maxage={cacheDuration}, immutable";
}
protected void SetFailureCacheHeader() {
int cacheDuration = (int)TimeSpan.FromMinutes(5).TotalSeconds;
Response.Headers.CacheControl = $"public, max-age={cacheDuration}, s-maxage={cacheDuration}, immutable";
}
}
+2 -1
View File
@@ -10,7 +10,8 @@ if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("APPSETTINGS_P
builder.Configuration.AddJsonFile(Environment.GetEnvironmentVariable("APPSETTINGS_PATH")!);
// Add services to the container.
builder.Services.AddSingleton<IFileSource>(new ProxyFileSource("http://cdn.old.server.spacebar.chat"));
// builder.Services.AddSingleton<IFileSource>(new ProxyFileSource("http://cdn.old.server.spacebar.chat"));
builder.Services.AddSingleton<IFileSource>(new FilesystemFileSource(Environment.GetEnvironmentVariable("STORAGE_PATH") ?? throw new InvalidOperationException("STORAGE_PATH not set!")));
builder.Services.AddSingleton<LruFileCache>(new LruFileCache(1 * 1024 * 1024 * 1024));
builder.Services.AddSingleton<PixelArtDetectionService>();
builder.Services.AddSingleton<DiscordImageResizeService>();
@@ -12,31 +12,31 @@ namespace Spacebar.UApi.Controllers;
[Route("/api/v{_}/guilds/{guildId}/stickers/")]
public class GuildStickerController(ILogger<MessagesController> logger, SpacebarDbContext db, SpacebarAspNetAuthenticationService authService, UApiConfiguration cfg, PermissionService permService, Config sbCfg) : ControllerBase {
// TODO proper response type
[HttpPost]
public async Task<Sticker> UploadGuildSticker(string guildId, MultipartFormDataContent content) {
var sticker = new Sticker() {
GuildId = guildId
};
foreach (var item in content) {
switch (item.Headers.ContentDisposition.Name.Trim('"')) {
case "name":
sticker.Name = await item.ReadAsStringAsync();
break;
case "description":
sticker.Description = await item.ReadAsStringAsync();
break;
case "tags":
sticker.Tags = await item.ReadAsStringAsync();
break;
case "file":
var fileContent = await item.ReadAsStreamAsync();
break;
}
}
return sticker;
}
// [HttpPost]
// public async Task<Sticker> UploadGuildSticker(string guildId, MultipartFormDataContent content) {
//
// var sticker = new Sticker() {
// GuildId = guildId
// };
//
// foreach (var item in content) {
// switch (item.Headers.ContentDisposition.Name.Trim('"')) {
// case "name":
// sticker.Name = await item.ReadAsStringAsync();
// break;
// case "description":
// sticker.Description = await item.ReadAsStringAsync();
// break;
// case "tags":
// sticker.Tags = await item.ReadAsStringAsync();
// break;
// case "file":
// var fileContent = await item.ReadAsStreamAsync();
//
// break;
// }
// }
//
// return sticker;
// }
}
@@ -11,42 +11,42 @@ namespace Spacebar.UApi.Controllers.Messages;
[ApiController]
[Route("/api/v{_}/channels/{channelId}/messages")]
public partial class MessagesController(ILogger<MessagesController> logger, SpacebarDbContext db, SpacebarAspNetAuthenticationService authService, UApiConfiguration cfg) : ControllerBase {
[Consumes("multipart/form-data")]
[HttpPost]
public async Task CreateMessageWithAttachments(string channelId, MultipartFormDataContent formData) {
// Generic proxy doesnt work with multipart/form-data for some reason, so handle them specially
JsonObject jsonPayload = null!;
foreach (var content in formData)
{
if (content.Headers.ContentDisposition?.Name == "payload_json") {
jsonPayload = await content.ReadFromJsonAsync<JsonObject>();
break;
}
if (FileNameRegex().IsMatch(content.Headers.ContentDisposition?.Name ?? "")) {
break;
}
throw new InvalidOperationException("Invalid multipart/form-data payload: missing payload_json or file attachments");
}
var client = new StreamingHttpClient();
var requestMessage = new HttpRequestMessage(
new HttpMethod(Request.Method),
cfg.FallbackApiEndpoint + Request.Path + Request.QueryString
) {
Content = new StreamContent(Request.Body)
};
Console.WriteLine(requestMessage.RequestUri);
var responseMessage = await client.SendUnhandledAsync(requestMessage, CancellationToken.None);
Response.StatusCode = (int)responseMessage.StatusCode;
foreach (var header in responseMessage.Headers) Response.Headers[header.Key] = header.Value.ToArray();
foreach (var header in responseMessage.Content.Headers) Response.Headers[header.Key] = header.Value.ToArray();
await responseMessage.Content.CopyToAsync(Response.Body);
}
// [Consumes("multipart/form-data")]
// [HttpPost]
// public async Task CreateMessageWithAttachments(string channelId, MultipartFormDataContent formData) {
// // Generic proxy doesnt work with multipart/form-data for some reason, so handle them specially
// JsonObject jsonPayload = null!;
//
// foreach (var content in formData)
// {
// if (content.Headers.ContentDisposition?.Name == "payload_json") {
// jsonPayload = await content.ReadFromJsonAsync<JsonObject>();
// break;
// }
// if (FileNameRegex().IsMatch(content.Headers.ContentDisposition?.Name ?? "")) {
//
// break;
// }
// throw new InvalidOperationException("Invalid multipart/form-data payload: missing payload_json or file attachments");
// }
//
// var client = new StreamingHttpClient();
// var requestMessage = new HttpRequestMessage(
// new HttpMethod(Request.Method),
// cfg.FallbackApiEndpoint + Request.Path + Request.QueryString
// ) {
// Content = new StreamContent(Request.Body)
// };
// Console.WriteLine(requestMessage.RequestUri);
//
// var responseMessage = await client.SendUnhandledAsync(requestMessage, CancellationToken.None);
// Response.StatusCode = (int)responseMessage.StatusCode;
//
// foreach (var header in responseMessage.Headers) Response.Headers[header.Key] = header.Value.ToArray();
// foreach (var header in responseMessage.Content.Headers) Response.Headers[header.Key] = header.Value.ToArray();
//
// await responseMessage.Content.CopyToAsync(Response.Body);
// }
[GeneratedRegex(@"files\[\d+\]")]
private static partial Regex FileNameRegex();
@@ -33,7 +33,7 @@ public class FsckService(ILogger<FsckService> logger, IServiceScopeFactory servi
}
private async Task RunFsckAsync(string name, string path, IQueryable<FsckItem> items) {
int i = 0, count = await items.CountAsync();
int i = 0, count = await items.CountAsync(), missing = 0;
List<Task> tasks = [];
await foreach (var item in items.AsAsyncEnumerable()) {
@@ -46,12 +46,16 @@ public class FsckService(ILogger<FsckService> logger, IServiceScopeFactory servi
i++;
if (!item.IsSingleSubDirFile) {
if (!await fs.FileExists(item.Path))
if (!await fs.FileExists(item.Path)) {
logger.LogWarning("{itemType} {itemId} is missing at {path}", name, item.ItemId, item.Path);
missing++;
}
}
else if (item.IsSingleSubDirFile && fs is FilesystemFileSource ffs) {
if(!await ffs.DirectoryExists(Path.GetDirectoryName(item.Path)))
if (!await ffs.DirectoryExists(Path.GetDirectoryName(item.Path))) {
logger.LogWarning("{itemType} {itemId} is missing at {path} (directory missing)", name, item.ItemId, item.Path);
missing++;
}
}
else {
logger.LogWarning("Unhandled case: {itemType} {itemId} -> {path} (IsSingleSubDirFile: {isSingleSubDirFile}, fstype: {fsType})", name, item.ItemId, item.Path, item.IsSingleSubDirFile, fs.GetType().Name);
@@ -62,7 +66,7 @@ public class FsckService(ILogger<FsckService> logger, IServiceScopeFactory servi
}
await Task.WhenAll(tasks);
logger.LogInformation("Validated {count} items for {path}.", i, path);
logger.LogInformation("Validated {count} items for {path}, {missing} missing.", i, path, missing);
}
#region User Assets
@@ -112,17 +116,15 @@ public class FsckService(ILogger<FsckService> logger, IServiceScopeFactory servi
.OrderBy(x => x.Id)
.Select(x => new FsckItem {
Path = $"/stickers/{x.Id}",
ItemId = x.Id,
IsSingleSubDirFile = fs is FilesystemFileSource
ItemId = x.Id
});
public IQueryable<FsckItem> EnumerateEmojiPathsAsync() =>
_db.Emojis
.OrderBy(x => x.Id)
.Select(x => new FsckItem {
Path = $"/emojis/{x.Id}/",
ItemId = x.Id,
IsSingleSubDirFile = fs is FilesystemFileSource
Path = $"/emojis/{x.Id}",
ItemId = x.Id
});
#endregion
+6 -2
View File
@@ -16,7 +16,7 @@
self,
nixpkgs,
flake-utils,
pion-webrtc
pion-webrtc,
}:
nixpkgs.lib.recursiveUpdate
(
@@ -102,6 +102,10 @@
// {
nixosModules.default = import ./nix/modules/default self;
nixosConfigurations.testVm = import ./nix/testVm/default.nix { inherit self nixpkgs; };
nixosConfigurations.test = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ];
};
checks =
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
@@ -125,4 +129,4 @@
inherit self nixpkgs flake-utils;
}
);
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ let
};
}
// (
if cfg.enableAdminApi then
if cfg.adminApi.enable then
{
adminApi = {
endpointPublic = "http${if cfg.adminApiEndpoint.useSsl then "s" else ""}://${cfg.adminApiEndpoint.host}:${toString cfg.adminApiEndpoint.publicPort}";
+65
View File
@@ -0,0 +1,65 @@
self:
{
config,
lib,
pkgs,
spacebar,
...
}:
let
secrets = import ../secrets.nix { inherit lib config; };
cfg = config.services.spacebarchat-server;
jsonFormat = pkgs.formats.json { };
in
{
imports = [ ];
options.services.spacebarchat-server.cdnCs = lib.mkOption {
default = { };
description = "Configuration for C# cdn.";
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "Enable experimental C# CDN.";
extraConfiguration = lib.mkOption {
type = jsonFormat.type;
default = import ./default-appsettings-json.nix;
description = "Extra appsettings.json configuration for the gateway offload daemon.";
};
};
};
};
config = lib.mkIf cfg.cdnCs.enable (
let
makeServerTsService = import ../makeServerTsService.nix { inherit cfg lib secrets; };
in
{
assertions = [
(import ./assert-has-connection-string.nix "Admin API" cfg.adminApi.extraConfiguration)
];
systemd.services.spacebar-cdn = makeServerTsService {
description = "Spacebar Server - CDN (C#)";
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;
ASPNETCORE_URLS = "http://0.0.0.0:${toString cfg.cdnEndpoint.localPort}";
STORAGE_LOCATION = cfg.cdnPath;
APPSETTINGS_PATH = jsonFormat.generate "appsettings.spacebar-cdn.json" (lib.recursiveUpdate (import ./default-appsettings-json.nix) cfg.cdnCs.extraConfiguration);
}
);
serviceConfig = {
ExecStart = "${self.packages.${pkgs.stdenv.hostPlatform.system}.Spacebar-AdminApi}/bin/Spacebar.AdminApi";
};
};
}
);
}
+1
View File
@@ -45,6 +45,7 @@ in
systemd.services.spacebar-uapi = makeServerTsService {
description = "Spacebar Server - C# API overlay";
# after = [ "spacebar-api.service" ];
environment = builtins.mapAttrs (_: val: builtins.toString val) (
{
# things we set by default...
+3 -3
View File
@@ -18,6 +18,7 @@ in
./integration-nginx.nix
./users.nix
(import ./pion-sfu.nix self)
(import ./cs/cdn-cs.nix self)
(import ./cs/gateway-offload-cs.nix self)
(import ./cs/admin-api.nix self)
(import ./cs/uapi.nix self)
@@ -28,8 +29,6 @@ in
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;
@@ -129,6 +128,7 @@ in
systemd.services.spacebar-gateway = makeServerTsService {
description = "Spacebar Server - Gateway";
# after = [ "spacebar-api.service" ];
environment = builtins.mapAttrs (_: val: builtins.toString val) (
{
# things we set by default...
@@ -150,7 +150,7 @@ in
};
};
systemd.services.spacebar-cdn = lib.mkIf (!cfg.enableCdnCs) (makeServerTsService {
systemd.services.spacebar-cdn = lib.mkIf (!cfg.cdnCs.enable) (makeServerTsService {
description = "Spacebar Server - CDN";
environment = builtins.mapAttrs (_: val: builtins.toString val) (
{
+7
View File
@@ -11,4 +11,11 @@
else
80;
};
mkEndpointRaw = domain: port: publicPort: ssl: {
host = domain;
localPort = port;
useSsl = ssl;
publicPort = publicPort;
};
}
@@ -75,6 +75,7 @@ lib.recursiveUpdate
StateDirectoryMode = "0750";
RuntimeDirectory = "spacebar";
RuntimeDirectoryMode = "0750";
RuntimeDirectoryPreserve = "yes";
ReadWritePaths = [ cfg.cdnPath ];
NoExecPaths = [ cfg.cdnPath ];
+39 -40
View File
@@ -1,4 +1,9 @@
{ pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
nginxTestSigning = {
addSSL = true;
@@ -11,72 +16,66 @@ let
in
{
networking.hostName = "sbtest";
services.nginx.virtualHosts."sb.localhost" = nginxTestSigning;
services.nginx.virtualHosts."api.sb.localhost" = nginxTestSigning;
services.nginx.virtualHosts."gw.sb.localhost" = nginxTestSigning;
services.nginx.virtualHosts."cdn.sb.localhost" = nginxTestSigning;
services.nginx.virtualHosts."webrtc.sb.localhost" = nginxTestSigning;
services.nginx.virtualHosts."admin.sb.localhost" = nginxTestSigning;
services.nginx.virtualHosts.${config.services.spacebarchat-server.serverName} = nginxTestSigning;
services.nginx.virtualHosts.${config.services.spacebarchat-server.adminApiEndpoint.host} = nginxTestSigning;
services.nginx.virtualHosts.${config.services.spacebarchat-server.apiEndpoint.host} = nginxTestSigning;
services.nginx.virtualHosts.${config.services.spacebarchat-server.cdnEndpoint.host} = nginxTestSigning;
services.nginx.virtualHosts.${config.services.spacebarchat-server.gatewayEndpoint.host} = nginxTestSigning;
services.nginx.virtualHosts.${config.services.spacebarchat-server.webrtcEndpoint.host} = nginxTestSigning;
services.spacebarchat-server =
let
sbLib = import ../modules/default/lib.nix;
csConnectionString = "Host=127.0.0.1; Username=postgres; Password=postgres; Database=spacebar; Port=5432; Include Error Detail=true; Maximum Pool Size=1000; Command Timeout=6000; Timeout=600;";
cfg = {
enable = true;
apiEndpoint = {
useSsl = false;
host = "api.sb.localhost";
localPort = 3001;
publicPort = 8080;
};
gatewayEndpoint = {
useSsl = false;
host = "gw.sb.localhost";
localPort = 3002;
publicPort = 8080;
};
cdnEndpoint = {
useSsl = false;
host = "cdn.sb.localhost";
localPort = 3003;
publicPort = 8080;
};
adminApiEndpoint = {
useSsl = false;
host = "admin.sb.localhost";
localPort = 3004;
publicPort = 8080;
};
webrtcEndpoint = {
useSsl = false;
host = "voice.sb.localhost";
localPort = 3005;
publicPort = 8080;
};
apiEndpoint = sbLib.mkEndpointRaw "api.sb.localhost" 3001 8080 false;
gatewayEndpoint = sbLib.mkEndpointRaw "gw.sb.localhost" 3002 8080 false;
cdnEndpoint = sbLib.mkEndpointRaw "cdn.sb.localhost" 3003 8080 false;
adminApiEndpoint = sbLib.mkEndpointRaw "admin.sb.localhost" 3004 8080 false;
webrtcEndpoint = sbLib.mkEndpointRaw "voice.sb.localhost" 3005 8080 false;
nginx.enable = true;
serverName = "sb.localhost";
settings = {
security = {
requestSignature = "meow";
cdnSignatureDuration = "5m";
cdnSignatureIncludeIp = true;
cdnSignatureIncludeUserAgent = false;
cdnSignatureKey = "meow";
};
};
gatewayOffload = {
enable = true;
enableGuildSync = true;
extraConfiguration.ConnectionStrings.Spacebar = csConnectionString;
};
adminApi = {
enable = true;
extraConfiguration.ConnectionStrings.Spacebar = csConnectionString;
};
cdnCs = {
enable = false;
extraConfiguration.ConnectionStrings.Spacebar = csConnectionString;
};
uApi = {
enable = true;
extraConfiguration.ConnectionStrings.Spacebar = csConnectionString;
};
pion-sfu = {
enable = true;
publicIp = "127.0.0.1";
};
extraEnvironment = {
DATABASE = "postgres://postgres:postgres@127.0.0.1/spacebar";
#WEBRTC_PORT_RANGE=60000-61000;
#PUBLIC_IP=216.230.228.60;
LOG_REQUESTS = "-200,204,304";
# LOG_REQUESTS = "-200,204,304";
LOG_REQUESTS = "-";
LOG_VALIDATION_ERRORS = true;
#DB_LOGGING=true;
#LOG_GATEWAY_TRACES=true;