mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
refactor(api): collapse single-impl interfaces and facades (#3382)
This commit is contained in:
1 parent
e7b13fd19f
commit
c16d414d93
426 files changed
+2085
-6445
No files matched your search
@@ -1,8 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {VirusScanProviderResult} from '@pkgs/virus_scan/src/VirusScanProviderResult';
|
||||
|
||||
export interface IVirusScanProvider {
|
||||
scanFile(filePath: string): Promise<VirusScanProviderResult>;
|
||||
scanBuffer(buffer: Buffer): Promise<VirusScanProviderResult>;
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
import {createReadStream} from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface';
|
||||
import type {IVirusHashCache} from '@pkgs/virus_scan/src/cache/IVirusHashCache';
|
||||
import type {IVirusScanFailureReporter} from '@pkgs/virus_scan/src/failures/IVirusScanFailureReporter';
|
||||
import {NoopVirusScanFailureReporter} from '@pkgs/virus_scan/src/failures/NoopVirusScanFailureReporter';
|
||||
import type {IVirusScanProvider} from '@pkgs/virus_scan/src/IVirusScanProvider';
|
||||
import type {IVirusScanService} from '@pkgs/virus_scan/src/IVirusScanService';
|
||||
import type {VirusScanResult} from '@pkgs/virus_scan/src/VirusScanResult';
|
||||
|
||||
interface VirusScanConfig {
|
||||
failOpen: boolean;
|
||||
cachedThreatLabel?: string;
|
||||
}
|
||||
|
||||
interface VirusScanServiceDependencies {
|
||||
provider: IVirusScanProvider;
|
||||
virusHashCache: IVirusHashCache;
|
||||
logger: LoggerInterface;
|
||||
config: VirusScanConfig;
|
||||
failureReporter?: IVirusScanFailureReporter;
|
||||
}
|
||||
|
||||
export class VirusScanService implements IVirusScanService {
|
||||
readonly enabled = true;
|
||||
private readonly cachedThreatLabel: string;
|
||||
private readonly failureReporter: IVirusScanFailureReporter;
|
||||
|
||||
constructor(private dependencies: VirusScanServiceDependencies) {
|
||||
this.cachedThreatLabel = dependencies.config.cachedThreatLabel ?? 'Cached virus signature';
|
||||
this.failureReporter = dependencies.failureReporter ?? new NoopVirusScanFailureReporter();
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await this.failureReporter.initialize();
|
||||
}
|
||||
|
||||
async scanFile(filePath: string): Promise<VirusScanResult> {
|
||||
const filename = path.basename(filePath);
|
||||
const fileHash = await this.hashFile(filePath);
|
||||
const isCachedVirus = await this.dependencies.virusHashCache.isKnownVirusHash(fileHash);
|
||||
if (isCachedVirus) {
|
||||
return {
|
||||
isClean: false,
|
||||
threat: this.cachedThreatLabel,
|
||||
fileHash,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const scanResult = await this.dependencies.provider.scanFile(filePath);
|
||||
if (scanResult.isClean) {
|
||||
return {
|
||||
isClean: true,
|
||||
fileHash,
|
||||
};
|
||||
}
|
||||
if (!scanResult.threat) {
|
||||
throw new Error('Virus scan provider returned infected status without threat name');
|
||||
}
|
||||
await this.dependencies.virusHashCache.cacheVirusHash(fileHash);
|
||||
return {
|
||||
isClean: false,
|
||||
threat: scanResult.threat,
|
||||
fileHash,
|
||||
};
|
||||
} catch (error) {
|
||||
this.dependencies.logger.error(
|
||||
{
|
||||
error: this.describeError(error),
|
||||
filename,
|
||||
fileHash,
|
||||
},
|
||||
'Virus scan failed',
|
||||
);
|
||||
await this.reportScanFailure(error, filename, fileHash);
|
||||
if (this.dependencies.config.failOpen) {
|
||||
return {
|
||||
isClean: true,
|
||||
fileHash,
|
||||
};
|
||||
}
|
||||
throw new Error(`Virus scan failed: ${this.describeError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async scanBuffer(buffer: Buffer, filename: string): Promise<VirusScanResult> {
|
||||
const fileHash = this.createFileHashFromBuffer(buffer);
|
||||
const isCachedVirus = await this.dependencies.virusHashCache.isKnownVirusHash(fileHash);
|
||||
if (isCachedVirus) {
|
||||
return {
|
||||
isClean: false,
|
||||
threat: this.cachedThreatLabel,
|
||||
fileHash,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const scanResult = await this.dependencies.provider.scanBuffer(buffer);
|
||||
if (scanResult.isClean) {
|
||||
return {
|
||||
isClean: true,
|
||||
fileHash,
|
||||
};
|
||||
}
|
||||
if (!scanResult.threat) {
|
||||
throw new Error('Virus scan provider returned infected status without threat name');
|
||||
}
|
||||
await this.dependencies.virusHashCache.cacheVirusHash(fileHash);
|
||||
return {
|
||||
isClean: false,
|
||||
threat: scanResult.threat,
|
||||
fileHash,
|
||||
};
|
||||
} catch (error) {
|
||||
this.dependencies.logger.error(
|
||||
{
|
||||
error: this.describeError(error),
|
||||
filename,
|
||||
fileHash,
|
||||
},
|
||||
'Virus scan failed',
|
||||
);
|
||||
await this.reportScanFailure(error, filename, fileHash);
|
||||
if (this.dependencies.config.failOpen) {
|
||||
return {
|
||||
isClean: true,
|
||||
fileHash,
|
||||
};
|
||||
}
|
||||
throw new Error(`Virus scan failed: ${this.describeError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
private createFileHashFromBuffer(buffer: Buffer): string {
|
||||
return crypto.createHash('sha256').update(buffer).digest('hex');
|
||||
}
|
||||
|
||||
private async hashFile(filePath: string): Promise<string> {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const stream = createReadStream(filePath, {highWaterMark: 1024 * 1024});
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
hash.update(chunk as Buffer);
|
||||
}
|
||||
} catch (error) {
|
||||
stream.destroy();
|
||||
throw error;
|
||||
}
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
private describeError(error: unknown): string {
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
private async reportScanFailure(error: unknown, filename: string, fileHash: string): Promise<void> {
|
||||
try {
|
||||
await this.failureReporter.reportFailure({
|
||||
error,
|
||||
filename,
|
||||
fileHash,
|
||||
failOpen: this.dependencies.config.failOpen,
|
||||
});
|
||||
} catch (reportError) {
|
||||
this.dependencies.logger.warn(
|
||||
{
|
||||
error: this.describeError(reportError),
|
||||
filename,
|
||||
fileHash,
|
||||
},
|
||||
'Failed to report virus scan failure',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export interface IVirusHashCache {
|
||||
isKnownVirusHash(fileHash: string): Promise<boolean>;
|
||||
cacheVirusHash(fileHash: string): Promise<void>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export interface IVirusScanCacheStore {
|
||||
get<T>(key: string): Promise<T | null>;
|
||||
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {IVirusHashCache} from '@pkgs/virus_scan/src/cache/IVirusHashCache';
|
||||
import type {IVirusScanCacheStore} from '@pkgs/virus_scan/src/cache/IVirusScanCacheStore';
|
||||
import {seconds} from 'itty-time';
|
||||
|
||||
interface VirusHashCacheConfig {
|
||||
keyPrefix?: string;
|
||||
ttlSeconds?: number;
|
||||
}
|
||||
|
||||
export class VirusHashCache implements IVirusHashCache {
|
||||
private readonly keyPrefix: string;
|
||||
private readonly ttlSeconds: number;
|
||||
|
||||
constructor(
|
||||
private cacheStore: IVirusScanCacheStore,
|
||||
config: VirusHashCacheConfig = {},
|
||||
) {
|
||||
this.keyPrefix = config.keyPrefix ?? 'virus';
|
||||
this.ttlSeconds = config.ttlSeconds ?? seconds('7 days');
|
||||
}
|
||||
|
||||
async isKnownVirusHash(fileHash: string): Promise<boolean> {
|
||||
const cachedValue = await this.cacheStore.get(this.buildCacheKey(fileHash));
|
||||
return cachedValue != null;
|
||||
}
|
||||
|
||||
async cacheVirusHash(fileHash: string): Promise<void> {
|
||||
await this.cacheStore.set(this.buildCacheKey(fileHash), 'true', this.ttlSeconds);
|
||||
}
|
||||
|
||||
private buildCacheKey(fileHash: string): string {
|
||||
return `${this.keyPrefix}:${fileHash}`;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {VirusScanFailureContext} from '@pkgs/virus_scan/src/failures/VirusScanFailureContext';
|
||||
|
||||
export interface IVirusScanFailureReporter {
|
||||
initialize(): Promise<void>;
|
||||
reportFailure(context: VirusScanFailureContext): Promise<void>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {IVirusScanFailureReporter} from '@pkgs/virus_scan/src/failures/IVirusScanFailureReporter';
|
||||
import type {VirusScanFailureContext} from '@pkgs/virus_scan/src/failures/VirusScanFailureContext';
|
||||
|
||||
export class NoopVirusScanFailureReporter implements IVirusScanFailureReporter {
|
||||
async initialize(): Promise<void> {}
|
||||
|
||||
async reportFailure(_context: VirusScanFailureContext): Promise<void> {}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export interface VirusScanFailureContext {
|
||||
error: unknown;
|
||||
filename: string;
|
||||
fileHash: string;
|
||||
failOpen: boolean;
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
import {createReadStream} from 'node:fs';
|
||||
import {createConnection} from 'node:net';
|
||||
import {Readable} from 'node:stream';
|
||||
import type {IVirusScanProvider} from '@pkgs/virus_scan/src/IVirusScanProvider';
|
||||
import type {VirusScanProviderResult} from '@pkgs/virus_scan/src/VirusScanProviderResult';
|
||||
|
||||
interface ClamAVConfig {
|
||||
@@ -12,7 +11,7 @@ interface ClamAVConfig {
|
||||
streamChunkBytes?: number;
|
||||
}
|
||||
|
||||
export class ClamAVProvider implements IVirusScanProvider {
|
||||
export class ClamAVProvider {
|
||||
private readonly streamChunkBytes: number;
|
||||
|
||||
constructor(private config: ClamAVConfig) {
|
||||
|
||||
Reference in new issue
Block a user