pipework 0.67.0 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/REFERENCE.md +54 -0
- package/dist/core/config/namespace.d.ts +1 -0
- package/dist/core/config/namespace.d.ts.map +1 -1
- package/dist/core/config/schema.d.ts +3 -0
- package/dist/core/config/schema.d.ts.map +1 -1
- package/dist/core/config/schema.js +2 -0
- package/dist/core/config/schema.js.map +1 -1
- package/dist/data/migrate/internal-definitions.d.ts.map +1 -1
- package/dist/data/migrate/internal-definitions.js +4 -0
- package/dist/data/migrate/internal-definitions.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/infra/aws/sigv4.d.ts +46 -0
- package/dist/infra/aws/sigv4.d.ts.map +1 -0
- package/dist/infra/aws/sigv4.js +94 -0
- package/dist/infra/aws/sigv4.js.map +1 -0
- package/dist/infra/email/index.d.ts +1 -1
- package/dist/infra/email/index.d.ts.map +1 -1
- package/dist/infra/email/index.js +1 -1
- package/dist/infra/email/index.js.map +1 -1
- package/dist/infra/email/providers.d.ts +1 -1
- package/dist/infra/email/providers.d.ts.map +1 -1
- package/dist/infra/email/providers.js +1 -1
- package/dist/infra/email/providers.js.map +1 -1
- package/dist/infra/storage/backend.d.ts +32 -0
- package/dist/infra/storage/backend.d.ts.map +1 -0
- package/dist/infra/storage/backend.js +2 -0
- package/dist/infra/storage/backend.js.map +1 -0
- package/dist/infra/storage/handle.d.ts +110 -0
- package/dist/infra/storage/handle.d.ts.map +1 -0
- package/dist/infra/storage/handle.js +317 -0
- package/dist/infra/storage/handle.js.map +1 -0
- package/dist/infra/storage/index.d.ts +8 -0
- package/dist/infra/storage/index.d.ts.map +1 -0
- package/dist/infra/storage/index.js +4 -0
- package/dist/infra/storage/index.js.map +1 -0
- package/dist/infra/storage/local.d.ts +16 -0
- package/dist/infra/storage/local.d.ts.map +1 -0
- package/dist/infra/storage/local.js +95 -0
- package/dist/infra/storage/local.js.map +1 -0
- package/dist/infra/storage/namespace.d.ts +13 -0
- package/dist/infra/storage/namespace.d.ts.map +1 -0
- package/dist/infra/storage/namespace.js +13 -0
- package/dist/infra/storage/namespace.js.map +1 -0
- package/dist/infra/storage/s3.d.ts +34 -0
- package/dist/infra/storage/s3.d.ts.map +1 -0
- package/dist/infra/storage/s3.js +220 -0
- package/dist/infra/storage/s3.js.map +1 -0
- package/dist/infra/storage/schema.d.ts +43 -0
- package/dist/infra/storage/schema.d.ts.map +1 -0
- package/dist/infra/storage/schema.js +23 -0
- package/dist/infra/storage/schema.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream';
|
|
2
|
+
/** One stored object as a listing reports it. `key` is relative to the backend's prefix. */
|
|
3
|
+
export interface BackendObject {
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly lastModified: Date;
|
|
6
|
+
}
|
|
7
|
+
export interface BackendSignedUrlOptions {
|
|
8
|
+
readonly expiresInSeconds: number;
|
|
9
|
+
readonly disposition: 'attachment' | 'inline';
|
|
10
|
+
readonly filename?: string;
|
|
11
|
+
readonly mediaType: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Where the bytes of stored files live. `storage.s3(...)` and `storage.local(...)`
|
|
15
|
+
* make one; keys are the relative `tenants/<tenantId>/...` names the handle builds.
|
|
16
|
+
*/
|
|
17
|
+
export interface StorageBackend {
|
|
18
|
+
readonly name: 's3' | 'local';
|
|
19
|
+
/** Streams `body` to `key`, replacing any object there. A stream error leaves nothing at `key`. */
|
|
20
|
+
put(key: string, body: AsyncIterable<Buffer | Uint8Array>): Promise<void>;
|
|
21
|
+
exists(key: string): Promise<boolean>;
|
|
22
|
+
copy(from: string, to: string): Promise<void>;
|
|
23
|
+
/** Removes an object; absent is not an error. */
|
|
24
|
+
remove(key: string): Promise<void>;
|
|
25
|
+
/** Streams an object's bytes. */
|
|
26
|
+
read(key: string): Promise<Readable>;
|
|
27
|
+
/** Every object whose key starts with `prefix`, in key order. */
|
|
28
|
+
list(prefix: string): AsyncIterable<BackendObject>;
|
|
29
|
+
/** A time-limited URL that serves the object to a client with no credentials; throws where the backend cannot. */
|
|
30
|
+
signedUrl(key: string, options: BackendSignedUrlOptions): Promise<string>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../../src/infra/storage/backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,YAAY,EAAE,IAAI,CAAA;CAC5B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,QAAQ,CAAC,WAAW,EAAE,YAAY,GAAG,QAAQ,CAAA;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAA;IAC7B,mGAAmG;IACnG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,GAAG,UAAU,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACrC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,iDAAiD;IACjD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,iCAAiC;IACjC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACpC,iEAAiE;IACjE,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,aAAa,CAAC,CAAA;IAClD,kHAAkH;IAClH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CAC1E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../../src/infra/storage/backend.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream';
|
|
2
|
+
import type { StorageBackend } from './backend.js';
|
|
3
|
+
export interface StorageConfig {
|
|
4
|
+
readonly backend: StorageBackend;
|
|
5
|
+
/** Largest file `put` accepts, in bytes. Default 1 GiB; at most 5 GiB. */
|
|
6
|
+
readonly maxBytes?: number;
|
|
7
|
+
/** The app's environment. The local backend is refused when this is `production`. */
|
|
8
|
+
readonly environment?: string;
|
|
9
|
+
}
|
|
10
|
+
/** The metadata row of a stored file. */
|
|
11
|
+
export interface StoredFile {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly tenantId: string;
|
|
14
|
+
/** Lowercase hex SHA-256 of the content. */
|
|
15
|
+
readonly sha256: string;
|
|
16
|
+
readonly size: number;
|
|
17
|
+
readonly mediaType: string;
|
|
18
|
+
readonly filename: string | null;
|
|
19
|
+
readonly createdAt: Date;
|
|
20
|
+
/** The Flow's `auth.userId` when `put` ran. */
|
|
21
|
+
readonly createdBy: string | null;
|
|
22
|
+
/** `null` = kept until deleted. */
|
|
23
|
+
readonly expiresAt: Date | null;
|
|
24
|
+
readonly metadata: unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface PutOptions {
|
|
27
|
+
/** The bytes. Read once, never held whole; the length need not be known. */
|
|
28
|
+
readonly body: Readable;
|
|
29
|
+
readonly mediaType: string;
|
|
30
|
+
readonly filename?: string;
|
|
31
|
+
/** Hex SHA-256 the content must have; a different digest fails with 422 and stores nothing. */
|
|
32
|
+
readonly expectedSha256?: string;
|
|
33
|
+
/** After this time the file is no longer returned and `sweep` deletes it. */
|
|
34
|
+
readonly expiresAt?: Date;
|
|
35
|
+
readonly metadata?: unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface SignedUrlOptions {
|
|
38
|
+
/** 1 to 604800 (seven days). Default 900. */
|
|
39
|
+
readonly expiresInSeconds?: number;
|
|
40
|
+
/** `'attachment'` (default) downloads; `'inline'` lets the browser display it. */
|
|
41
|
+
readonly disposition?: 'attachment' | 'inline';
|
|
42
|
+
/** Name the browser saves under; default the stored filename. */
|
|
43
|
+
readonly filename?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface SweepOptions {
|
|
46
|
+
/** Most rows expired and most objects deleted in one call (each). Default 1000. */
|
|
47
|
+
readonly limit?: number;
|
|
48
|
+
}
|
|
49
|
+
export interface SweepResult {
|
|
50
|
+
/** Rows past `expiresAt` deleted. */
|
|
51
|
+
readonly expired: number;
|
|
52
|
+
/** Content objects deleted because no row referenced them. */
|
|
53
|
+
readonly objects: number;
|
|
54
|
+
/** Abandoned upload temp objects (over 24 hours old) deleted. */
|
|
55
|
+
readonly temp: number;
|
|
56
|
+
}
|
|
57
|
+
export interface Storage {
|
|
58
|
+
/**
|
|
59
|
+
* Stores a stream for the Flow's tenant and returns its metadata row. The bytes are written to a
|
|
60
|
+
* temporary object while hashed and counted, then moved to the tenant's content address
|
|
61
|
+
* (`tenants/<tenantId>/sha256/<hex>`); the same bytes put again by the tenant add a row and no
|
|
62
|
+
* object. Over `maxBytes` throws `PayloadTooLargeError` (413); a digest other than `expectedSha256`
|
|
63
|
+
* throws `ValidationError` (422); either leaves no row and no object. The blob is written first
|
|
64
|
+
* and the row in the Flow's transaction, so a rolled-back transaction leaves an object that
|
|
65
|
+
* `sweep()` removes. An `StoredFile:put` audit record is written when the Flow has an open trace step.
|
|
66
|
+
*/
|
|
67
|
+
put(options: PutOptions): Promise<StoredFile>;
|
|
68
|
+
putFor(tenantId: string, options: PutOptions): Promise<StoredFile>;
|
|
69
|
+
/** The file's row, or `null` when there is none (or it has expired). */
|
|
70
|
+
get(id: string): Promise<StoredFile | null>;
|
|
71
|
+
getFor(tenantId: string, id: string): Promise<StoredFile | null>;
|
|
72
|
+
/** The row and a stream of the content (pass `file.size` as `length` to `http.file()`). Throws `NotFoundError` when there is no row. */
|
|
73
|
+
open(id: string): Promise<{
|
|
74
|
+
file: StoredFile;
|
|
75
|
+
stream: Readable;
|
|
76
|
+
}>;
|
|
77
|
+
openFor(tenantId: string, id: string): Promise<{
|
|
78
|
+
file: StoredFile;
|
|
79
|
+
stream: Readable;
|
|
80
|
+
}>;
|
|
81
|
+
/**
|
|
82
|
+
* A URL that serves the content to anyone holding it until it expires, with the requested
|
|
83
|
+
* Content-Disposition and the stored media type. S3 backend only; the local backend throws.
|
|
84
|
+
*/
|
|
85
|
+
signedUrl(id: string, options?: SignedUrlOptions): Promise<{
|
|
86
|
+
url: string;
|
|
87
|
+
expiresAt: Date;
|
|
88
|
+
}>;
|
|
89
|
+
signedUrlFor(tenantId: string, id: string, options?: SignedUrlOptions): Promise<{
|
|
90
|
+
url: string;
|
|
91
|
+
expiresAt: Date;
|
|
92
|
+
}>;
|
|
93
|
+
/** Deletes the row (audit `StoredFile:delete`); the object goes at the next `sweep()` once no row references it. Returns whether there was a row. */
|
|
94
|
+
delete(id: string): Promise<boolean>;
|
|
95
|
+
deleteFor(tenantId: string, id: string): Promise<boolean>;
|
|
96
|
+
/**
|
|
97
|
+
* For a cron (system, all tenants). Deletes rows past `expiresAt` (audit `StoredFile:expire`), then
|
|
98
|
+
* content objects no row references and upload temp objects over 24 hours old. Needs a Flow
|
|
99
|
+
* (a job or schedule run). Audit records need an open trace step, as in `put`.
|
|
100
|
+
*/
|
|
101
|
+
sweep(options?: SweepOptions): Promise<SweepResult>;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Tenant-scoped, content-addressed file storage over a backend (`storage.s3(...)` or
|
|
105
|
+
* `storage.local(...)`). Requires blueprint `storage: {}` for the `storage_file` table.
|
|
106
|
+
* Throws when the local backend is used with `environment: 'production'`, or `maxBytes`
|
|
107
|
+
* is not an integer from 1 to 5 GiB.
|
|
108
|
+
*/
|
|
109
|
+
export declare function createStorage(config: StorageConfig): Storage;
|
|
110
|
+
//# sourceMappingURL=handle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../../../src/infra/storage/handle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { sql } from '../../data/db/sql.js';
|
|
3
|
+
import { pipe, pipeSystem } from '../../data/db/db.js';
|
|
4
|
+
import { requireContext } from '../../request/context/store.js';
|
|
5
|
+
import { inTenant } from '../../auth/tenant/in-tenant.js';
|
|
6
|
+
import { resolveActorId } from '../../data/write/audit/actor.js';
|
|
7
|
+
import { getBaseLogger } from '../../core/logging/index.js';
|
|
8
|
+
import { NotFoundError, PayloadTooLargeError, ValidationError } from '../../core/errors/index.js';
|
|
9
|
+
const DEFAULT_MAX_BYTES = 1024 ** 3;
|
|
10
|
+
const MAX_BACKEND_BYTES = 5 * 1024 ** 3;
|
|
11
|
+
const MAX_SIGNED_SECONDS = 604800;
|
|
12
|
+
const DEFAULT_SIGNED_SECONDS = 900;
|
|
13
|
+
const DEFAULT_SWEEP_LIMIT = 1000;
|
|
14
|
+
const TMP_MAX_AGE_MS = 24 * 3_600_000;
|
|
15
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16
|
+
const OBJECT_KEY = /^tenants\/([0-9a-f-]{36})\/sha256\/([0-9a-f]{64})$/;
|
|
17
|
+
const TMP_KEY = /^tenants\/[0-9a-f-]{36}\/tmp\/[0-9a-f-]{36}$/;
|
|
18
|
+
function toDate(value) {
|
|
19
|
+
if (value instanceof Date)
|
|
20
|
+
return value;
|
|
21
|
+
const text = String(value);
|
|
22
|
+
return new Date(/(Z|[+-]\d\d(:?\d\d)?)$/.test(text) ? text : `${text.replace(' ', 'T')}Z`);
|
|
23
|
+
}
|
|
24
|
+
function rowToFile(row) {
|
|
25
|
+
const expires = row['expires_at'];
|
|
26
|
+
return {
|
|
27
|
+
id: row['id'],
|
|
28
|
+
tenantId: row['tenant_id'],
|
|
29
|
+
sha256: row['sha256'],
|
|
30
|
+
size: Number(row['size']),
|
|
31
|
+
mediaType: row['media_type'],
|
|
32
|
+
filename: row['filename'] ?? null,
|
|
33
|
+
createdAt: toDate(row['created_at']),
|
|
34
|
+
createdBy: row['created_by'] ?? null,
|
|
35
|
+
expiresAt: expires === null || expires === undefined ? null : toDate(expires),
|
|
36
|
+
metadata: row['metadata'] ?? null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function auditProjection(file) {
|
|
40
|
+
return {
|
|
41
|
+
id: file.id, sha256: file.sha256, size: file.size, mediaType: file.mediaType, filename: file.filename,
|
|
42
|
+
createdBy: file.createdBy, expiresAt: file.expiresAt,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function requireId(name, value) {
|
|
46
|
+
if (typeof value !== 'string' || !UUID.test(value)) {
|
|
47
|
+
throw new ValidationError({ [name]: ['must be a UUID'] }, `Storage ${name} must be a UUID.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function keyFor(tenantId, sha256) {
|
|
51
|
+
return `tenants/${tenantId.toLowerCase()}/sha256/${sha256}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Tenant-scoped, content-addressed file storage over a backend (`storage.s3(...)` or
|
|
55
|
+
* `storage.local(...)`). Requires blueprint `storage: {}` for the `storage_file` table.
|
|
56
|
+
* Throws when the local backend is used with `environment: 'production'`, or `maxBytes`
|
|
57
|
+
* is not an integer from 1 to 5 GiB.
|
|
58
|
+
*/
|
|
59
|
+
export function createStorage(config) {
|
|
60
|
+
const backend = config.backend;
|
|
61
|
+
const maxBytes = config.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
62
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_BACKEND_BYTES) {
|
|
63
|
+
throw new Error('[pipework] storage maxBytes must be an integer from 1 to 5 GiB (5368709120).\n');
|
|
64
|
+
}
|
|
65
|
+
if (backend.name === 'local' && config.environment === 'production') {
|
|
66
|
+
throw new Error('[pipework] storage.local is refused in production.\n\n' +
|
|
67
|
+
' It keeps files on this machine\'s disk. Configure storage.s3({ bucket, region, credentials }).\n');
|
|
68
|
+
}
|
|
69
|
+
function tenantOf(explicit, flow) {
|
|
70
|
+
if (explicit !== null) {
|
|
71
|
+
requireId('tenantId', explicit);
|
|
72
|
+
return explicit;
|
|
73
|
+
}
|
|
74
|
+
if (flow.tenant === null) {
|
|
75
|
+
throw new Error('[pipework] This storage method needs a tenant in the Flow.\n\n' +
|
|
76
|
+
' Call it from an authenticated tenant route, or use the ...For(tenantId, ...) form\n' +
|
|
77
|
+
' (putFor, getFor, openFor, ...) from a webhook or a job with no tenant.\n');
|
|
78
|
+
}
|
|
79
|
+
return flow.tenant;
|
|
80
|
+
}
|
|
81
|
+
async function run(explicit, fn) {
|
|
82
|
+
const flow = requireContext();
|
|
83
|
+
const tenantId = tenantOf(explicit, flow);
|
|
84
|
+
if (explicit !== null)
|
|
85
|
+
return inTenant(flow, tenantId, tx => fn(tx, tenantId, flow));
|
|
86
|
+
return pipe.transaction((tx) => fn(tx, tenantId, flow));
|
|
87
|
+
}
|
|
88
|
+
async function audit(flow, tx, tenantId, verb, fileId, details) {
|
|
89
|
+
const trace = flow.trace;
|
|
90
|
+
const stepId = trace?.buffer.openStepId ?? null;
|
|
91
|
+
if (trace === null || stepId === null)
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
await tx.transaction(async (sp) => {
|
|
95
|
+
await sp.execute(sql `
|
|
96
|
+
INSERT INTO audit_record
|
|
97
|
+
(trace_day, trace_id, trace_step_id, tenant_id, actor_id, entity, verb, entity_id, after, occurred_at)
|
|
98
|
+
VALUES (${trace.openedDay}, ${trace.id}, ${stepId}, ${tenantId}, ${resolveActorId(flow)},
|
|
99
|
+
'StoredFile', ${verb}, ${fileId}, ${JSON.stringify(details)}::jsonb, now())
|
|
100
|
+
`);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
getBaseLogger().error({ err, traceId: trace.id }, `Failed to write the stored file ${verb} audit record`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// One tenant-and-content lock orders a put's "does the object exist, then the row" against a
|
|
108
|
+
// sweep's "is there a row, then delete the object". Held to the end of the transaction.
|
|
109
|
+
async function lockContent(tx, tenantId, sha256) {
|
|
110
|
+
await tx.execute(sql `SELECT pg_advisory_xact_lock(hashtextextended(${`pipework.storage:${tenantId.toLowerCase()}:${sha256}`}, 0))`);
|
|
111
|
+
}
|
|
112
|
+
async function discard(key) {
|
|
113
|
+
try {
|
|
114
|
+
await backend.remove(key);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
getBaseLogger().error({ err, key }, 'storage failed to remove a temporary object; sweep() will');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function doPut(explicit, options) {
|
|
121
|
+
if (typeof options.mediaType !== 'string' || options.mediaType.trim() === '' || /[\r\n]/.test(options.mediaType)) {
|
|
122
|
+
throw new ValidationError({ mediaType: ['must be a media type such as "application/pdf"'] }, 'Storage mediaType must be a non-empty single line.');
|
|
123
|
+
}
|
|
124
|
+
let expected = null;
|
|
125
|
+
if (options.expectedSha256 !== undefined) {
|
|
126
|
+
if (!/^[0-9a-f]{64}$/i.test(options.expectedSha256)) {
|
|
127
|
+
throw new ValidationError({ expectedSha256: ['must be 64 hex characters'] }, 'Storage expectedSha256 must be a hex SHA-256.');
|
|
128
|
+
}
|
|
129
|
+
expected = options.expectedSha256.toLowerCase();
|
|
130
|
+
}
|
|
131
|
+
if (options.expiresAt !== undefined && !(options.expiresAt instanceof Date && !Number.isNaN(options.expiresAt.getTime()))) {
|
|
132
|
+
throw new ValidationError({ expiresAt: ['must be a valid Date'] }, 'Storage expiresAt must be a Date.');
|
|
133
|
+
}
|
|
134
|
+
// Resolve the tenant before any byte is written.
|
|
135
|
+
const flow = requireContext();
|
|
136
|
+
const tenantId = tenantOf(explicit, flow);
|
|
137
|
+
const hash = createHash('sha256');
|
|
138
|
+
let size = 0;
|
|
139
|
+
const measured = (async function* () {
|
|
140
|
+
for await (const chunk of options.body) {
|
|
141
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
142
|
+
size += buf.length;
|
|
143
|
+
if (size > maxBytes)
|
|
144
|
+
throw new PayloadTooLargeError(`File is larger than ${maxBytes} bytes`, { maxBytes });
|
|
145
|
+
hash.update(buf);
|
|
146
|
+
yield buf;
|
|
147
|
+
}
|
|
148
|
+
})();
|
|
149
|
+
const tmpKey = `tenants/${tenantId.toLowerCase()}/tmp/${randomUUID()}`;
|
|
150
|
+
try {
|
|
151
|
+
await backend.put(tmpKey, measured);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
options.body.destroy();
|
|
155
|
+
await discard(tmpKey);
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
const sha256 = hash.digest('hex');
|
|
159
|
+
if (expected !== null && expected !== sha256) {
|
|
160
|
+
await discard(tmpKey);
|
|
161
|
+
throw new ValidationError({ expectedSha256: [`content has sha256 ${sha256}`] }, 'The uploaded content does not match expectedSha256.');
|
|
162
|
+
}
|
|
163
|
+
const finalKey = keyFor(tenantId, sha256);
|
|
164
|
+
try {
|
|
165
|
+
return await run(explicit, async (tx, tenant, f) => {
|
|
166
|
+
await lockContent(tx, tenant, sha256);
|
|
167
|
+
if (!(await backend.exists(finalKey)))
|
|
168
|
+
await backend.copy(tmpKey, finalKey);
|
|
169
|
+
const metadata = options.metadata === undefined ? null : JSON.stringify(options.metadata);
|
|
170
|
+
const rows = await tx.execute(sql `
|
|
171
|
+
INSERT INTO storage_file (tenant_id, sha256, size, media_type, filename, created_by, expires_at, metadata)
|
|
172
|
+
VALUES (${tenant}, ${sha256}, ${size}, ${options.mediaType.trim()}, ${options.filename ?? null},
|
|
173
|
+
${resolveActorId(f)}, ${options.expiresAt?.toISOString() ?? null},
|
|
174
|
+
${metadata === null ? sql `NULL` : sql `${metadata}::jsonb`})
|
|
175
|
+
RETURNING *
|
|
176
|
+
`);
|
|
177
|
+
const file = rowToFile(rows[0]);
|
|
178
|
+
await audit(f, tx, tenant, 'put', file.id, auditProjection(file));
|
|
179
|
+
return file;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
await discard(tmpKey);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function findFile(tx, tenantId, id, includeExpired) {
|
|
187
|
+
const rows = await tx.execute(includeExpired
|
|
188
|
+
? sql `SELECT * FROM storage_file WHERE tenant_id = ${tenantId} AND id = ${id}`
|
|
189
|
+
: sql `SELECT * FROM storage_file WHERE tenant_id = ${tenantId} AND id = ${id} AND (expires_at IS NULL OR expires_at > now())`);
|
|
190
|
+
return rows.length === 0 ? null : rowToFile(rows[0]);
|
|
191
|
+
}
|
|
192
|
+
async function doGet(explicit, id) {
|
|
193
|
+
requireId('id', id);
|
|
194
|
+
return run(explicit, (tx, tenantId) => findFile(tx, tenantId, id, false));
|
|
195
|
+
}
|
|
196
|
+
async function doOpen(explicit, id) {
|
|
197
|
+
const file = await doGet(explicit, id);
|
|
198
|
+
if (file === null)
|
|
199
|
+
throw new NotFoundError('File', { id });
|
|
200
|
+
return { file, stream: await backend.read(keyFor(file.tenantId, file.sha256)) };
|
|
201
|
+
}
|
|
202
|
+
async function doSignedUrl(explicit, id, options = {}) {
|
|
203
|
+
const seconds = options.expiresInSeconds ?? DEFAULT_SIGNED_SECONDS;
|
|
204
|
+
if (!Number.isInteger(seconds) || seconds < 1 || seconds > MAX_SIGNED_SECONDS) {
|
|
205
|
+
throw new ValidationError({ expiresInSeconds: [`must be an integer from 1 to ${MAX_SIGNED_SECONDS}`] }, 'Storage expiresInSeconds is out of range.');
|
|
206
|
+
}
|
|
207
|
+
const disposition = options.disposition ?? 'attachment';
|
|
208
|
+
if (disposition !== 'attachment' && disposition !== 'inline') {
|
|
209
|
+
throw new ValidationError({ disposition: ["must be 'attachment' or 'inline'"] }, 'Storage disposition must be attachment or inline.');
|
|
210
|
+
}
|
|
211
|
+
const file = await doGet(explicit, id);
|
|
212
|
+
if (file === null)
|
|
213
|
+
throw new NotFoundError('File', { id });
|
|
214
|
+
const filename = options.filename ?? file.filename ?? undefined;
|
|
215
|
+
const issued = Date.now();
|
|
216
|
+
const url = await backend.signedUrl(keyFor(file.tenantId, file.sha256), {
|
|
217
|
+
expiresInSeconds: seconds, disposition: disposition, mediaType: file.mediaType, ...(filename !== undefined ? { filename } : {}),
|
|
218
|
+
});
|
|
219
|
+
return { url, expiresAt: new Date(issued + seconds * 1000) };
|
|
220
|
+
}
|
|
221
|
+
async function doDelete(explicit, id) {
|
|
222
|
+
requireId('id', id);
|
|
223
|
+
return run(explicit, async (tx, tenantId, flow) => {
|
|
224
|
+
const rows = await tx.execute(sql `DELETE FROM storage_file WHERE tenant_id = ${tenantId} AND id = ${id} RETURNING *`);
|
|
225
|
+
if (rows.length === 0)
|
|
226
|
+
return false;
|
|
227
|
+
const file = rowToFile(rows[0]);
|
|
228
|
+
await audit(flow, tx, tenantId, 'delete', file.id, auditProjection(file));
|
|
229
|
+
return true;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async function doSweep(options = {}) {
|
|
233
|
+
const limit = options.limit ?? DEFAULT_SWEEP_LIMIT;
|
|
234
|
+
if (!Number.isSafeInteger(limit) || limit < 1) {
|
|
235
|
+
throw new ValidationError({ limit: ['must be a positive integer'] }, 'Storage sweep limit must be a positive integer.');
|
|
236
|
+
}
|
|
237
|
+
const flow = requireContext();
|
|
238
|
+
let expired = 0;
|
|
239
|
+
let objects = 0;
|
|
240
|
+
let temp = 0;
|
|
241
|
+
await pipeSystem().transaction(async (tx) => {
|
|
242
|
+
const rows = await tx.execute(sql `
|
|
243
|
+
DELETE FROM storage_file WHERE id IN (
|
|
244
|
+
SELECT id FROM storage_file WHERE expires_at IS NOT NULL AND expires_at <= now()
|
|
245
|
+
ORDER BY expires_at LIMIT ${limit} FOR UPDATE SKIP LOCKED
|
|
246
|
+
) RETURNING *
|
|
247
|
+
`);
|
|
248
|
+
for (const row of rows) {
|
|
249
|
+
const file = rowToFile(row);
|
|
250
|
+
await audit(flow, tx, file.tenantId, 'expire', file.id, auditProjection(file));
|
|
251
|
+
}
|
|
252
|
+
expired = rows.length;
|
|
253
|
+
});
|
|
254
|
+
const cutoff = Date.now() - TMP_MAX_AGE_MS;
|
|
255
|
+
let batch = [];
|
|
256
|
+
const settle = async () => {
|
|
257
|
+
if (batch.length === 0)
|
|
258
|
+
return;
|
|
259
|
+
const candidates = batch;
|
|
260
|
+
batch = [];
|
|
261
|
+
const present = await pipeSystem().execute(sql `
|
|
262
|
+
SELECT tenant_id::text AS t, sha256 AS h FROM storage_file
|
|
263
|
+
WHERE tenant_id::text || ':' || sha256 IN (SELECT jsonb_array_elements_text(${JSON.stringify(candidates.map(c => `${c.tenantId}:${c.sha256}`))}::jsonb))
|
|
264
|
+
`);
|
|
265
|
+
const referenced = new Set(present.map(r => `${r['t']}:${r['h']}`));
|
|
266
|
+
for (const c of candidates) {
|
|
267
|
+
if (objects >= limit)
|
|
268
|
+
return;
|
|
269
|
+
if (referenced.has(`${c.tenantId}:${c.sha256}`))
|
|
270
|
+
continue;
|
|
271
|
+
const removed = await pipeSystem().transaction(async (tx) => {
|
|
272
|
+
await lockContent(tx, c.tenantId, c.sha256);
|
|
273
|
+
const again = await tx.execute(sql `
|
|
274
|
+
SELECT 1 FROM storage_file WHERE tenant_id = ${c.tenantId} AND sha256 = ${c.sha256} LIMIT 1
|
|
275
|
+
`);
|
|
276
|
+
if (again.length > 0)
|
|
277
|
+
return false;
|
|
278
|
+
await backend.remove(c.key);
|
|
279
|
+
return true;
|
|
280
|
+
});
|
|
281
|
+
if (removed)
|
|
282
|
+
objects += 1;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
for await (const object of backend.list('tenants/')) {
|
|
286
|
+
if (objects >= limit && temp >= limit)
|
|
287
|
+
break;
|
|
288
|
+
const match = OBJECT_KEY.exec(object.key);
|
|
289
|
+
if (match !== null) {
|
|
290
|
+
if (objects < limit)
|
|
291
|
+
batch.push({ tenantId: match[1], sha256: match[2], key: object.key });
|
|
292
|
+
if (batch.length >= 200)
|
|
293
|
+
await settle();
|
|
294
|
+
}
|
|
295
|
+
else if (TMP_KEY.test(object.key) && object.lastModified.getTime() < cutoff && temp < limit) {
|
|
296
|
+
await backend.remove(object.key);
|
|
297
|
+
temp += 1;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
await settle();
|
|
301
|
+
return { expired, objects, temp };
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
put: o => doPut(null, o),
|
|
305
|
+
putFor: (t, o) => doPut(t, o),
|
|
306
|
+
get: id => doGet(null, id),
|
|
307
|
+
getFor: (t, id) => doGet(t, id),
|
|
308
|
+
open: id => doOpen(null, id),
|
|
309
|
+
openFor: (t, id) => doOpen(t, id),
|
|
310
|
+
signedUrl: (id, o) => doSignedUrl(null, id, o),
|
|
311
|
+
signedUrlFor: (t, id, o) => doSignedUrl(t, id, o),
|
|
312
|
+
delete: id => doDelete(null, id),
|
|
313
|
+
deleteFor: (t, id) => doDelete(t, id),
|
|
314
|
+
sweep: o => doSweep(o),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=handle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle.js","sourceRoot":"","sources":["../../../src/infra/storage/handle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createStorage } from './handle.js';
|
|
2
|
+
export { s3, encryptionHeaders, PART_SIZE } from './s3.js';
|
|
3
|
+
export { local } from './local.js';
|
|
4
|
+
export type { S3Config, S3Encryption } from './s3.js';
|
|
5
|
+
export type { LocalConfig } from './local.js';
|
|
6
|
+
export type { StorageBackend, BackendObject, BackendSignedUrlOptions } from './backend.js';
|
|
7
|
+
export type { Storage, StorageConfig, StoredFile, PutOptions, SignedUrlOptions, SweepOptions, SweepResult, } from './handle.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/infra/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,EAAE,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACrD,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAC7C,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAC1F,YAAY,EACV,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAE,YAAY,EAAE,WAAW,GAC5F,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/infra/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,EAAE,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StorageBackend } from './backend.js';
|
|
2
|
+
export interface LocalConfig {
|
|
3
|
+
/** Directory the files are written under; created on first write. */
|
|
4
|
+
readonly dir: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Files on local disk, for development and tests. Refused by `storage.create` when
|
|
8
|
+
* its `environment` is `production`. There are no signed URLs: `signedUrl` throws
|
|
9
|
+
* here, so serve a file by streaming `open()` through `http.file()`.
|
|
10
|
+
*
|
|
11
|
+
* In a test, give each file its own directory (`mkdtemp`) and remove it in `afterAll`;
|
|
12
|
+
* rows written through the test harness roll back with its transaction, and the
|
|
13
|
+
* leftover blobs are then plain files in that directory.
|
|
14
|
+
*/
|
|
15
|
+
export declare function local(config: LocalConfig): StorageBackend;
|
|
16
|
+
//# sourceMappingURL=local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../../src/infra/storage/local.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,cAAc,CAAA;AAEjE,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,cAAc,CAiFzD"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
2
|
+
import { copyFile, mkdir, readdir, rename, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
import { Readable } from 'node:stream';
|
|
7
|
+
/**
|
|
8
|
+
* Files on local disk, for development and tests. Refused by `storage.create` when
|
|
9
|
+
* its `environment` is `production`. There are no signed URLs: `signedUrl` throws
|
|
10
|
+
* here, so serve a file by streaming `open()` through `http.file()`.
|
|
11
|
+
*
|
|
12
|
+
* In a test, give each file its own directory (`mkdtemp`) and remove it in `afterAll`;
|
|
13
|
+
* rows written through the test harness roll back with its transaction, and the
|
|
14
|
+
* leftover blobs are then plain files in that directory.
|
|
15
|
+
*/
|
|
16
|
+
export function local(config) {
|
|
17
|
+
const root = resolve(config.dir);
|
|
18
|
+
function pathOf(key) {
|
|
19
|
+
const full = resolve(root, ...key.split('/'));
|
|
20
|
+
if (full !== root && !full.startsWith(root + sep)) {
|
|
21
|
+
throw new Error(`[pipework] storage local: key "${key}" escapes the storage directory.\n`);
|
|
22
|
+
}
|
|
23
|
+
return full;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
name: 'local',
|
|
27
|
+
async put(key, body) {
|
|
28
|
+
const target = pathOf(key);
|
|
29
|
+
await mkdir(dirname(target), { recursive: true });
|
|
30
|
+
const partial = `${target}.${randomUUID()}.partial`;
|
|
31
|
+
try {
|
|
32
|
+
await pipeline(Readable.from(body, { objectMode: false }), createWriteStream(partial));
|
|
33
|
+
await rename(partial, target);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
await rm(partial, { force: true });
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async exists(key) {
|
|
41
|
+
try {
|
|
42
|
+
return (await stat(pathOf(key))).isFile();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (err.code === 'ENOENT')
|
|
46
|
+
return false;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async copy(from, to) {
|
|
51
|
+
const target = pathOf(to);
|
|
52
|
+
await mkdir(dirname(target), { recursive: true });
|
|
53
|
+
await copyFile(pathOf(from), target);
|
|
54
|
+
},
|
|
55
|
+
async remove(key) {
|
|
56
|
+
await rm(pathOf(key), { force: true });
|
|
57
|
+
},
|
|
58
|
+
async read(key) {
|
|
59
|
+
const path = pathOf(key);
|
|
60
|
+
await stat(path);
|
|
61
|
+
return createReadStream(path);
|
|
62
|
+
},
|
|
63
|
+
async *list(prefix) {
|
|
64
|
+
const walk = async function* (dir) {
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err.code === 'ENOENT')
|
|
71
|
+
return;
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
75
|
+
const full = join(dir, entry.name);
|
|
76
|
+
if (entry.isDirectory())
|
|
77
|
+
yield* walk(full);
|
|
78
|
+
else if (entry.isFile() && !entry.name.endsWith('.partial'))
|
|
79
|
+
yield full;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
for await (const file of walk(root)) {
|
|
83
|
+
const key = file.slice(root.length + 1).split(sep).join('/');
|
|
84
|
+
if (!key.startsWith(prefix))
|
|
85
|
+
continue;
|
|
86
|
+
yield { key, lastModified: (await stat(file)).mtime };
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
signedUrl() {
|
|
90
|
+
return Promise.reject(new Error('[pipework] The local storage backend has no signed URLs.\n\n' +
|
|
91
|
+
' Stream the file with storage.open(id) and send it with http.file(), or use storage.s3(...).\n'));
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=local.js.map
|