mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
feat(api): route downloads through the configured downloads provider (#1802)
This commit is contained in:
1 parent
bdacaea4a8
commit
ddee116339
8 files changed
+79
-12
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import {resolveDownloadsProvider} from '@fluxer/config/src/S3DownloadsProvider';
|
||||||
import type {MasterConfig} from '@fluxer/config/src/MasterConfig';
|
import type {MasterConfig} from '@fluxer/config/src/MasterConfig';
|
||||||
import {parseIpAddress} from '@fluxer/ip_utils/src/IpAddress';
|
import {parseIpAddress} from '@fluxer/ip_utils/src/IpAddress';
|
||||||
import {parseGeoipSourceConfig, resolveGeoipRuntimeSourceConfig} from '@pkgs/geoip/src/GeoipStartup';
|
import {parseGeoipSourceConfig, resolveGeoipRuntimeSourceConfig} from '@pkgs/geoip/src/GeoipStartup';
|
||||||
@@ -237,6 +238,7 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
|||||||
cacheMinTtlSeconds: master.services.api.embeds.cache_min_ttl_seconds,
|
cacheMinTtlSeconds: master.services.api.embeds.cache_min_ttl_seconds,
|
||||||
cacheRespectRemoteTtl: master.services.api.embeds.cache_respect_remote_ttl,
|
cacheRespectRemoteTtl: master.services.api.embeds.cache_respect_remote_ttl,
|
||||||
},
|
},
|
||||||
|
s3Downloads: resolveDownloadsProvider(master),
|
||||||
s3: {
|
s3: {
|
||||||
endpoint: s3Config.endpoint,
|
endpoint: s3Config.endpoint,
|
||||||
presignedUrlBase: s3Config.presigned_url_base,
|
presignedUrlBase: s3Config.presigned_url_base,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import type {ResolvedDownloadsProvider} from '@fluxer/config/src/S3DownloadsProvider';
|
||||||
import type {WorkerTaskName} from '../worker/WorkerLaneConfig';
|
import type {WorkerTaskName} from '../worker/WorkerLaneConfig';
|
||||||
|
|
||||||
export type APIWorkerMode = 'all_lanes' | 'single_lane' | 'single_task';
|
export type APIWorkerMode = 'all_lanes' | 'single_lane' | 'single_task';
|
||||||
@@ -150,6 +151,7 @@ export interface APIConfig {
|
|||||||
static: string;
|
static: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
s3Downloads: ResolvedDownloadsProvider;
|
||||||
email: {
|
email: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
provider: 'smtp' | 'none';
|
provider: 'smtp' | 'none';
|
||||||
|
|||||||
@@ -187,3 +187,26 @@ describe('StorageService.copyObjectWithMetadataStripping', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('provider selection', () => {
|
||||||
|
interface ClientProbe {
|
||||||
|
client: {config: {region: () => Promise<string>; endpoint?: () => Promise<{hostname: string}>}};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('defaults to the shared S3 configuration', async () => {
|
||||||
|
const service = new StorageService() as unknown as ClientProbe;
|
||||||
|
expect(await service.client.config.region()).toBe(Config.s3.region);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses an explicitly supplied provider instead of the shared one', async () => {
|
||||||
|
const service = new StorageService({
|
||||||
|
endpoint: 'https://downloads.example.net',
|
||||||
|
forcePathStyle: false,
|
||||||
|
region: 'eu-central-9',
|
||||||
|
accessKeyId: 'DL_KEY',
|
||||||
|
secretAccessKey: 'DL_SECRET',
|
||||||
|
}) as unknown as ClientProbe;
|
||||||
|
expect(await service.client.config.region()).toBe('eu-central-9');
|
||||||
|
expect(await service.client.config.region()).not.toBe(Config.s3.region);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import type {S3ProviderSettings} from '@fluxer/config/src/S3DownloadsProvider';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import {createHash} from 'node:crypto';
|
import {createHash} from 'node:crypto';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
@@ -105,27 +106,36 @@ function extractStreamFromGet(out: GetObjectCommandOutput): Readable {
|
|||||||
export class StorageService implements IStorageService {
|
export class StorageService implements IStorageService {
|
||||||
private readonly client: S3Client;
|
private readonly client: S3Client;
|
||||||
private readonly presignClient: S3Client;
|
private readonly presignClient: S3Client;
|
||||||
|
private readonly provider: S3ProviderSettings;
|
||||||
|
|
||||||
constructor() {
|
constructor(provider?: S3ProviderSettings) {
|
||||||
this.client = buildPooledS3Client({
|
this.provider = provider ?? {
|
||||||
endpoint: Config.s3.endpoint,
|
endpoint: Config.s3.endpoint,
|
||||||
|
presignedUrlBase: Config.s3.presignedUrlBase,
|
||||||
|
forcePathStyle: Config.s3.forcePathStyle,
|
||||||
region: Config.s3.region,
|
region: Config.s3.region,
|
||||||
accessKeyId: Config.s3.accessKeyId,
|
accessKeyId: Config.s3.accessKeyId,
|
||||||
secretAccessKey: Config.s3.secretAccessKey,
|
secretAccessKey: Config.s3.secretAccessKey,
|
||||||
|
};
|
||||||
|
this.client = buildPooledS3Client({
|
||||||
|
endpoint: this.provider.endpoint,
|
||||||
|
region: this.provider.region,
|
||||||
|
accessKeyId: this.provider.accessKeyId,
|
||||||
|
secretAccessKey: this.provider.secretAccessKey,
|
||||||
forcePathStyle: true,
|
forcePathStyle: true,
|
||||||
});
|
});
|
||||||
this.presignClient = buildPooledS3Client({
|
this.presignClient = buildPooledS3Client({
|
||||||
endpoint: this.resolvePresignEndpoint(),
|
endpoint: this.resolvePresignEndpoint(),
|
||||||
region: Config.s3.region,
|
region: this.provider.region,
|
||||||
accessKeyId: Config.s3.accessKeyId,
|
accessKeyId: this.provider.accessKeyId,
|
||||||
secretAccessKey: Config.s3.secretAccessKey,
|
secretAccessKey: this.provider.secretAccessKey,
|
||||||
forcePathStyle: Config.s3.forcePathStyle,
|
forcePathStyle: this.provider.forcePathStyle,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolvePresignEndpoint(): string {
|
private resolvePresignEndpoint(): string {
|
||||||
const fallbackEndpoint = Config.s3.endpoint;
|
const fallbackEndpoint = this.provider.endpoint;
|
||||||
const configuredEndpoint = Config.s3.presignedUrlBase;
|
const configuredEndpoint = this.provider.presignedUrlBase;
|
||||||
if (!configuredEndpoint) {
|
if (!configuredEndpoint) {
|
||||||
return fallbackEndpoint;
|
return fallbackEndpoint;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import {describe, expect, it} from 'vitest';
|
||||||
|
import {Config} from '../Config';
|
||||||
|
import {createDownloadsStorageService} from './StorageServiceFactory';
|
||||||
|
|
||||||
|
describe('createDownloadsStorageService', () => {
|
||||||
|
it('returns null when no downloads override is configured', () => {
|
||||||
|
expect(Config.s3Downloads.isOverridden).toBe(false);
|
||||||
|
expect(createDownloadsStorageService()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the downloads provider to the shared provider by default', () => {
|
||||||
|
expect(Config.s3Downloads.settings.endpoint).toBe(Config.s3.endpoint);
|
||||||
|
expect(Config.s3Downloads.settings.region).toBe(Config.s3.region);
|
||||||
|
expect(Config.s3Downloads.settings.accessKeyId).toBe(Config.s3.accessKeyId);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import {Config} from '../Config';
|
||||||
import type {IStorageService} from './IStorageService';
|
import type {IStorageService} from './IStorageService';
|
||||||
import {StorageService} from './StorageService';
|
import {StorageService} from './StorageService';
|
||||||
|
|
||||||
export function createStorageService(): IStorageService {
|
export function createStorageService(): IStorageService {
|
||||||
return new StorageService();
|
return new StorageService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createDownloadsStorageService(): IStorageService | null {
|
||||||
|
if (!Config.s3Downloads.isOverridden) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new StorageService(Config.s3Downloads.settings);
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@ import {KVActivityTracker} from '../infrastructure/KVActivityTracker';
|
|||||||
import {KVBulkMessageDeletionQueueService} from '../infrastructure/KVBulkMessageDeletionQueueService';
|
import {KVBulkMessageDeletionQueueService} from '../infrastructure/KVBulkMessageDeletionQueueService';
|
||||||
import {NatsUnfurlerService} from '../infrastructure/NatsUnfurlerService';
|
import {NatsUnfurlerService} from '../infrastructure/NatsUnfurlerService';
|
||||||
import {PremiumStateReconciliationQueueService} from '../infrastructure/PremiumStateReconciliationQueueService';
|
import {PremiumStateReconciliationQueueService} from '../infrastructure/PremiumStateReconciliationQueueService';
|
||||||
import {createStorageService} from '../infrastructure/StorageServiceFactory';
|
import {createDownloadsStorageService, createStorageService} from '../infrastructure/StorageServiceFactory';
|
||||||
import {UserCacheService} from '../infrastructure/UserCacheService';
|
import {UserCacheService} from '../infrastructure/UserCacheService';
|
||||||
import {createUsersServiceClient} from '../infrastructure/UsersServiceClient';
|
import {createUsersServiceClient} from '../infrastructure/UsersServiceClient';
|
||||||
import {VirusScanService} from '../infrastructure/VirusScanService';
|
import {VirusScanService} from '../infrastructure/VirusScanService';
|
||||||
@@ -193,6 +193,10 @@ export const getStorageService: () => IStorageService = (() => {
|
|||||||
const fallback = singleton(() => createStorageService());
|
const fallback = singleton(() => createStorageService());
|
||||||
return () => _injectedStorageService ?? fallback();
|
return () => _injectedStorageService ?? fallback();
|
||||||
})();
|
})();
|
||||||
|
const getDownloadsStorageService: () => IStorageService = (() => {
|
||||||
|
const override = singleton(() => createDownloadsStorageService());
|
||||||
|
return () => override() ?? getStorageService();
|
||||||
|
})();
|
||||||
export const getErrorI18nService = singleton(() => new ErrorI18nService());
|
export const getErrorI18nService = singleton(() => new ErrorI18nService());
|
||||||
export const getLimitConfigService = singleton(
|
export const getLimitConfigService = singleton(
|
||||||
() => new LimitConfigService(getInstanceConfigRepository(), getCacheService(), getKVClient()),
|
() => new LimitConfigService(getInstanceConfigRepository(), getCacheService(), getKVClient()),
|
||||||
@@ -262,7 +266,7 @@ export function getKVAccountDeletionQueue(): KVAccountDeletionQueueService {
|
|||||||
return accountDeletionQueue;
|
return accountDeletionQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDownloadService = singleton(() => new DownloadService(getStorageService()));
|
export const getDownloadService = singleton(() => new DownloadService(getDownloadsStorageService()));
|
||||||
export const getThemeService = singleton(() => new ThemeService(getStorageService()));
|
export const getThemeService = singleton(() => new ThemeService(getStorageService()));
|
||||||
const getNcmecReporter = singleton(() => new NcmecReporter({config: createNcmecApiConfig(), fetch}));
|
const getNcmecReporter = singleton(() => new NcmecReporter({config: createNcmecApiConfig(), fetch}));
|
||||||
const getNcmecRepository = singleton(() => new NcmecRepository());
|
const getNcmecRepository = singleton(() => new NcmecRepository());
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type {MasterConfig} from '@fluxer/config/src/MasterConfig';
|
import type {MasterConfig} from '@fluxer/config/src/MasterConfig';
|
||||||
|
|
||||||
interface S3ProviderSettings {
|
export interface S3ProviderSettings {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
presignedUrlBase?: string;
|
presignedUrlBase?: string;
|
||||||
forcePathStyle: boolean;
|
forcePathStyle: boolean;
|
||||||
@@ -11,7 +11,7 @@ interface S3ProviderSettings {
|
|||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResolvedDownloadsProvider {
|
export interface ResolvedDownloadsProvider {
|
||||||
settings: S3ProviderSettings;
|
settings: S3ProviderSettings;
|
||||||
isOverridden: boolean;
|
isOverridden: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user