refactor: cleanup

This commit is contained in:
zhom
2026-08-27 09:16:25 +04:00
parent 63f673e7d4
commit 15b51f8d2d
31 changed files with 1471 additions and 339 deletions
+11
View File
@@ -1,3 +1,14 @@
# Storage for developing and testing donut-sync itself. It runs MinIO only, and
# the sync server is expected to run on the host beside it (`pnpm start:dev`),
# which is why MinIO is published and why the port matches the one pinned in
# test/test-env.ts.
#
# This is NOT the self-hosting compose file. That one runs donut-sync in a
# container too, and it must set S3_PUBLIC_ENDPOINT, because a server that signs
# presigned URLs against a compose-internal host such as `http://minio:9000`
# hands every device a URL it cannot open, while /health and /readyz stay green.
# Take the self-hosting compose from https://donutbrowser.com/docs/self-hosting
# rather than from here.
services:
minio:
image: minio/minio:latest
+35 -9
View File
@@ -86,6 +86,11 @@ export class SyncService implements OnModuleInit {
// `S3_PUBLIC_ENDPOINT` names a different, client-reachable address.
private presignClient: S3Client;
private publicEndpoint: string;
/**
* Whether an operator chose the public endpoint, or it fell back to the
* server's own storage address. The fallback is the shape that fails.
*/
private publicEndpointWasConfigured: boolean;
private bucket: string;
// Upper bound on presign batch array length (DoS guard).
private static readonly MAX_BATCH_ITEMS = 1000;
@@ -131,9 +136,11 @@ export class SyncService implements OnModuleInit {
// network and nowhere else. Signing is bound to the host, so the presign
// client is a second client pinned to the public address rather than a
// string rewrite of the signed URL.
const publicEndpoint =
this.configService.get<string>("S3_PUBLIC_ENDPOINT") || endpoint;
const configuredPublicEndpoint =
this.configService.get<string>("S3_PUBLIC_ENDPOINT");
const publicEndpoint = configuredPublicEndpoint || endpoint;
this.publicEndpoint = publicEndpoint;
this.publicEndpointWasConfigured = Boolean(configuredPublicEndpoint);
this.presignClient =
publicEndpoint === endpoint
? this.s3Client
@@ -191,14 +198,33 @@ export class SyncService implements OnModuleInit {
const isSingleLabel =
!host.includes(".") && !host.includes(":") && host !== "localhost";
if (!isSingleLabel) return;
this.logger.warn(
`Storage endpoint '${this.publicEndpoint}' uses the container-only host '${host}'. ` +
"Presigned URLs built from it cannot be reached by Donut Browser, so every " +
"transfer will fail while /health and /readyz stay green. Set S3_PUBLIC_ENDPOINT " +
"to an address your devices can reach (and publish that port).",
);
if (isSingleLabel) {
this.logger.warn(
`Storage endpoint '${this.publicEndpoint}' uses the container-only host '${host}'. ` +
"Presigned URLs built from it cannot be reached by Donut Browser, so every " +
"transfer will fail while /health and /readyz stay green. Set S3_PUBLIC_ENDPOINT " +
"to an address your devices can reach (and publish that port).",
);
return;
}
// A dotted host proves nothing. With `S3_PUBLIC_ENDPOINT` unset, clients are
// handed whatever address this server uses for storage itself, and a
// reachable-looking name such as `storage.internal`, or a private address on
// a network the devices are not on, fails in exactly the same way while
// saying nothing at all. This server cannot test the endpoint for them,
// because it does not know where its clients are, so state what it does
// know and leave the judgement to the operator.
if (!this.publicEndpointWasConfigured) {
this.logger.log(
`S3_PUBLIC_ENDPOINT is not set, so presigned URLs will name '${this.publicEndpoint}', ` +
"the address this server uses for storage itself. Transfers go straight from each " +
"device to that address, and this server cannot verify a device can reach it. If " +
"transfers fail while /health and /readyz stay green, set S3_PUBLIC_ENDPOINT to an " +
"address your devices can reach and publish that port.",
);
}
}
private async ensureBucketExists(): Promise<void> {
+70 -1
View File
@@ -1,4 +1,4 @@
import { INestApplication } from "@nestjs/common";
import { INestApplication, Logger } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { Test, TestingModule } from "@nestjs/testing";
import request from "supertest";
@@ -199,3 +199,72 @@ describe("presigned URL host", () => {
});
});
});
// The server cannot test whether a device can reach the endpoint it signs, so
// the only honest thing it can do is say what it is handing out. Without this,
// the one configuration that breaks every transfer boots completely silently.
describe("boot message about the presign endpoint", () => {
let logs: string[];
let warnings: string[];
let logSpy: jest.SpyInstance;
let warnSpy: jest.SpyInstance;
beforeEach(() => {
logs = [];
warnings = [];
logSpy = jest
.spyOn(Logger.prototype, "log")
.mockImplementation((message: unknown) => {
logs.push(String(message));
});
warnSpy = jest
.spyOn(Logger.prototype, "warn")
.mockImplementation((message: unknown) => {
warnings.push(String(message));
});
});
afterEach(() => {
logSpy.mockRestore();
warnSpy.mockRestore();
});
it("says which host clients will be handed when S3_PUBLIC_ENDPOINT is unset", async () => {
const app = await bootstrap(undefined);
try {
const spoken = [...logs, ...warnings].join("\n");
expect(spoken).toContain("S3_PUBLIC_ENDPOINT");
expect(spoken).toContain(TEST_S3_ENDPOINT);
} finally {
await app.close();
}
});
// A single-label host is the documented compose default and cannot work for
// any client, so it earns a warning rather than a note.
it("warns loudly about a container-only host", async () => {
const app = await bootstrap("http://minio:9000");
try {
const spoken = warnings.join("\n");
expect(spoken).toContain("minio");
expect(spoken).toContain("S3_PUBLIC_ENDPOINT");
} finally {
delete process.env.S3_PUBLIC_ENDPOINT;
await app.close();
}
});
// An operator who set the variable made a choice. Repeating the note at them
// would train them to ignore it, and the warning above is for the value that
// provably cannot work, not for every value the server cannot verify.
it("stays quiet when an operator has chosen a routable endpoint", async () => {
const app = await bootstrap(PUBLIC_ENDPOINT);
try {
const spoken = [...logs, ...warnings].join("\n");
expect(spoken).not.toContain("S3_PUBLIC_ENDPOINT is not set");
} finally {
delete process.env.S3_PUBLIC_ENDPOINT;
await app.close();
}
});
});