/
QuotaStrategy.ts
105 lines (94 loc) · 4.27 KB
/
QuotaStrategy.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// These two eslint lines are needed to store 'this' in a variable so it can be used
// in the PassThrough of createQuotaGuard
/* eslint-disable @typescript-eslint/no-this-alias */
/* eslint-disable consistent-this */
import { PassThrough } from 'stream';
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { PayloadHttpError } from '../../util/errors/PayloadHttpError';
import type { Guarded } from '../../util/GuardedStream';
import { guardStream } from '../../util/GuardedStream';
import type { Size } from '../size-reporter/Size';
import type { SizeReporter } from '../size-reporter/SizeReporter';
/**
* A QuotaStrategy is used when we want to set a limit to the amount of data that can be
* stored on the server.
* This can range from a limit for the whole server to a limit on a per pod basis.
* The way the size of a resource is calculated is implemented by the implementing classes.
* This can be bytes, quads, file count, ...
*/
export abstract class QuotaStrategy {
public readonly reporter: SizeReporter<any>;
public readonly limit: Size;
public constructor(reporter: SizeReporter<any>, limit: Size) {
this.reporter = reporter;
this.limit = limit;
}
/**
* Get the available space when writing data to the given identifier.
* If the given resource already exists it will deduct the already taken up
* space by that resource since it is going to be overwritten and thus counts
* as available space.
*
* @param identifier - the identifier of the resource of which you want the available space
* @returns the available space and the unit of the space as a Size object
*/
public async getAvailableSpace(identifier: ResourceIdentifier): Promise<Size> {
const totalUsed = await this.getTotalSpaceUsed(identifier);
// Ignore identifiers where quota does not apply
if (totalUsed.amount === Number.MAX_SAFE_INTEGER) {
return totalUsed;
}
// When a file is overwritten the space the file takes up right now should also
// be counted as available space as it will disappear/be overwritten
totalUsed.amount -= (await this.reporter.getSize(identifier)).amount;
return {
amount: this.limit.amount - totalUsed.amount,
unit: this.limit.unit,
};
}
/**
* Get the currently used/occupied space.
*
* @param identifier - the identifier that should be used to calculate the total
* @returns a Size object containing the requested value.
* If quota is not relevant for this identifier, Size.amount should be Number.MAX_SAFE_INTEGER
*/
protected abstract getTotalSpaceUsed(identifier: ResourceIdentifier): Promise<Size>;
/**
* Get an estimated size of the resource
*
* @param metadata - the metadata that might include the size
* @returns a Size object containing the estimated size and unit of the resource
*/
public async estimateSize(metadata: RepresentationMetadata): Promise<Size | undefined> {
const estimate = await this.reporter.estimateSize(metadata);
return estimate ? { unit: this.limit.unit, amount: estimate } : undefined;
}
/**
* Get a Passthrough stream that will keep track of the available space.
* If the quota is exceeded the stream will emit an error and destroy itself.
* Like other Passthrough instances this will simply pass on the chunks, when the quota isn't exceeded.
*
* @param identifier - the identifier of the resource in question
* @returns a Passthrough instance that errors when quota is exceeded
*/
public async createQuotaGuard(identifier: ResourceIdentifier): Promise<Guarded<PassThrough>> {
let total = 0;
const strategy = this;
const { reporter } = this;
return guardStream(new PassThrough({
async transform(this, chunk: any, enc: string, done: () => void): Promise<void> {
total += await reporter.calculateChunkSize(chunk);
const availableSpace = await strategy.getAvailableSpace(identifier);
if (availableSpace.amount < total) {
this.destroy(new PayloadHttpError(
`Quota exceeded by ${total - availableSpace.amount} ${availableSpace.unit} during write`,
));
}
this.push(chunk);
done();
},
}));
}
}