@podosoft/podokit 1.2.0 → 1.4.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/dist/add.d.ts +13 -2
- package/dist/add.js +135 -20
- package/dist/api-paths.d.ts +34 -0
- package/dist/api-paths.js +103 -0
- package/dist/deploy-compose-profile.d.ts +4 -0
- package/dist/deploy-compose-profile.js +13 -2
- package/dist/deploy-compose-render.d.ts +1 -1
- package/dist/deploy-compose-render.js +23 -7
- package/dist/deploy-compose.js +2 -1
- package/dist/deploy-profile.d.ts +3 -0
- package/dist/deploy-profile.js +12 -4
- package/dist/deploy-render.js +54 -8
- package/dist/deploy.js +21 -1
- package/dist/dev.d.ts +14 -0
- package/dist/dev.js +152 -10
- package/dist/index.js +23 -2
- package/dist/lib.d.ts +4 -2
- package/dist/lib.js +5 -2
- package/dist/lockfile.d.ts +6 -1
- package/dist/lockfile.js +26 -6
- package/dist/module-api-paths.d.ts +52 -0
- package/dist/module-api-paths.js +189 -0
- package/dist/remove.d.ts +3 -0
- package/dist/remove.js +12 -3
- package/dist/templates/fullstack/AGENTS.md +17 -2
- package/dist/templates/fullstack/README.md +19 -5
- package/dist/templates/fullstack/apps/api/package.json +1 -1
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +1 -0
- package/dist/templates/fullstack/apps/api/src/app.extensions.ts +7 -0
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +20 -1
- package/dist/templates/fullstack/apps/api/src/app.ts +11 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +3 -0
- package/dist/templates/fullstack/apps/api/src/config/redis.connection.spec.ts +48 -2
- package/dist/templates/fullstack/apps/api/src/config/redis.connection.ts +45 -0
- package/dist/templates/fullstack/apps/api/src/config/server-options.spec.ts +46 -0
- package/dist/templates/fullstack/apps/api/src/config/server-options.ts +48 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +1 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +10 -1
- package/dist/templates/fullstack/apps/api/src/core/services.spec.ts +196 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +181 -2
- package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +15 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +3 -0
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +8 -1
- package/dist/templates/fullstack/apps/api/src/database/database.ts +29 -10
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +1 -1
- package/dist/templates/fullstack/apps/api/src/database/pool-guard.spec.ts +188 -0
- package/dist/templates/fullstack/apps/api/src/database/pool-guard.ts +151 -0
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.spec.ts +28 -4
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.ts +10 -2
- package/dist/templates/fullstack/apps/api/src/main.ts +2 -1
- package/dist/templates/fullstack/apps/web/components.json +3 -1
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/lib/components/ui/textarea/index.ts +7 -0
- package/dist/templates/fullstack/apps/web/src/lib/components/ui/textarea/textarea.svelte +22 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +276 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +194 -9
- package/dist/templates/fullstack/apps/web/src/lib/server/body-limits.ts +15 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/proxy-headers.ts +12 -0
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +10 -3
- package/dist/templates/fullstack/dot-claude/skills/podokit-elysia-endpoint/SKILL.md +5 -0
- package/dist/templates/fullstack/dot-claude/skills/podokit-sveltekit-route/SKILL.md +14 -1
- package/dist/templates/fullstack/dot-devcontainer/devcontainer.json +1 -0
- package/dist/templates/fullstack/{dot-env.docker → dot-env.docker.example} +7 -4
- package/dist/templates/fullstack/dot-env.example +10 -0
- package/dist/templates/fullstack/dot-gitignore +1 -0
- package/dist/templates/fullstack/dot-podokit/dev.json +2 -1
- package/dist/templates/fullstack/infra/k3s/README.md +3 -1
- package/dist/templates/fullstack/tests/playwright.extensions.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +21 -26
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.migration.spec.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.spec.ts +76 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.ts +41 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.spec.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.ts +153 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/bootstrap-admin-runtime.ts +60 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/migrations/1792060000000-IndexSessionCreatedAt.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +27 -9
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.test.ts +59 -32
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.ts +81 -11
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/asset-paths.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.test.ts +23 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +2 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/public-paths.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/runtime-context.test.ts +125 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/runtime-context.ts +107 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +13 -19
- package/dist/templates/modules/admin-dashboard/files/tests/api/admin-users.api.spec.ts +86 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +1 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +15 -12
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/object-response.ts +12 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +0 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +3 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/request-budget.ui.spec.ts +14 -15
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +13 -8
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +14 -3
- package/dist/templates/modules/admin-dashboard/module.manifest.json +4 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +53 -11
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1792000000000-IndexAuditLogFilters.ts +26 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.spec.ts +26 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +17 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +8 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/database-outage.spec.ts +114 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/database-outage.ts +50 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.spec.ts +4 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +11 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/maintenance-command.ts +5 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/maintenance.ts +14 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/postgres-pool.spec.ts +136 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/postgres-pool.ts +40 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.spec.ts +53 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.ts +65 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +16 -37
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.spec.ts +33 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.ts +29 -0
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -2
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +2 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +121 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +108 -10
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.module.ts +3 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/queue.ts +31 -3
- package/dist/templates/modules/bullmq/module.manifest.json +5 -1
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/events-redis.module.ts +2 -4
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.spec.ts +224 -0
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.ts +124 -35
- package/dist/templates/modules/events-redis/module.manifest.json +4 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.routes.ts +136 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.spec.ts +636 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.ts +673 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.spec.ts +10 -4
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +37 -8
- package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-config.ts +99 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/migrations/1792100000000-InitFileUploadSessions.ts +49 -0
- package/dist/templates/modules/file-upload/files/apps/web/src/lib/uploads.test.ts +456 -0
- package/dist/templates/modules/file-upload/files/apps/web/src/lib/uploads.ts +356 -0
- package/dist/templates/modules/file-upload/files/tests/api/chunked-uploads.api.spec.ts +147 -0
- package/dist/templates/modules/file-upload/module.manifest.json +34 -6
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/jobs-local.module.ts +1 -1
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +23 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +30 -20
- package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.ts +20 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +79 -12
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +16 -1
- package/dist/templates/modules/object-storage-local/module.manifest.json +11 -2
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-multipart.spec.ts +115 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-multipart.ts +171 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.spec.ts +222 -9
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +55 -19
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-signer.spec.ts +71 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-signer.ts +98 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.spec.ts +6 -3
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.config.ts +7 -2
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +187 -44
- package/dist/templates/modules/object-storage-s3/module.manifest.json +10 -2
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +11 -22
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.spec.ts +113 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.ts +122 -0
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +24 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +7 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.spec.ts +14 -1
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +9 -12
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.module.ts +16 -4
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.spec.ts +233 -5
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.ts +134 -32
- package/dist/templates/modules/redis/module.manifest.json +4 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/event-stream.ts +11 -4
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.spec.ts +48 -1
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +15 -2
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +68 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +25 -1
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +46 -0
- package/dist/templates/modules/sse/module.manifest.json +3 -1
- package/dist/templates/todo/AGENTS.md +9 -1
- package/dist/templates/todo/README.md +18 -5
- package/dist/update.d.ts +22 -0
- package/dist/update.js +68 -8
- package/package.json +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-validation.ts +0 -13
- package/dist/templates/modules/auth/files/apps/api/src/auth/skip-schema-validation.ts +0 -5
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { AppException } from "@podosoft/podokit-contracts";
|
|
3
|
-
import type
|
|
3
|
+
import { objectBodyChunks, type ObjectBody, type ObjectInfo, type ObjectStore } from "@podosoft/podokit-runtime";
|
|
4
4
|
import { Elysia } from "elysia";
|
|
5
5
|
import type { RequestIdentity } from "../core/services";
|
|
6
6
|
import { filesRoutes } from "./files.module";
|
|
7
7
|
|
|
8
8
|
async function bytes(body: ObjectBody): Promise<Uint8Array> {
|
|
9
|
-
if (typeof body === "string") return Buffer.from(body);
|
|
10
|
-
if (body instanceof Uint8Array) return body;
|
|
11
9
|
const chunks: Uint8Array[] = [];
|
|
12
|
-
for await (const chunk of body) chunks.push(chunk);
|
|
10
|
+
for await (const chunk of objectBodyChunks(body)) chunks.push(chunk);
|
|
13
11
|
return Buffer.concat(chunks);
|
|
14
12
|
}
|
|
15
13
|
|
|
14
|
+
async function unused(): Promise<never> {
|
|
15
|
+
throw new Error("The files routes do not use multipart uploads");
|
|
16
|
+
}
|
|
17
|
+
|
|
16
18
|
function streamOf(value: Uint8Array): ReadableStream<Uint8Array> {
|
|
17
19
|
return new ReadableStream({
|
|
18
20
|
start(controller): void {
|
|
@@ -48,6 +50,10 @@ function memoryStore(): ObjectStore {
|
|
|
48
50
|
objects.delete(key);
|
|
49
51
|
},
|
|
50
52
|
getDownloadUrl: async (key) => `https://storage.example.com/${key}`,
|
|
53
|
+
createMultipartUpload: unused,
|
|
54
|
+
uploadPart: unused,
|
|
55
|
+
completeMultipartUpload: unused,
|
|
56
|
+
abortMultipartUpload: unused,
|
|
51
57
|
close: () => undefined,
|
|
52
58
|
};
|
|
53
59
|
}
|
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import { AppException } from "@podosoft/podokit-contracts";
|
|
2
2
|
import { Elysia, t } from "elysia";
|
|
3
|
-
import { OBJECT_STORAGE, type ObjectStore } from "@podosoft/podokit-runtime";
|
|
3
|
+
import { OBJECT_STORAGE, RATE_LIMIT_ROUTES, type ObjectStore } from "@podosoft/podokit-runtime";
|
|
4
|
+
import { apiServerOptions } from "../config/server-options";
|
|
4
5
|
import {
|
|
6
|
+
DATABASE,
|
|
7
|
+
LOGGER,
|
|
5
8
|
REQUEST_IDENTITY,
|
|
6
9
|
type AppPlugin,
|
|
7
10
|
type ElysiaPlugin,
|
|
8
11
|
type PodokitModule,
|
|
9
12
|
type RequestIdentity,
|
|
13
|
+
type ServiceKey,
|
|
10
14
|
} from "../core/services";
|
|
15
|
+
import { ChunkedUploads } from "./chunked-uploads";
|
|
16
|
+
import { allowedOrigins, chunkedUploadRoutes } from "./chunked-uploads.routes";
|
|
17
|
+
import { uploadConfig } from "./upload-config";
|
|
11
18
|
import { uploadAccess, uploadKey } from "./upload-keys";
|
|
12
19
|
|
|
20
|
+
export const CHUNKED_UPLOADS = Symbol("chunked-uploads") as ServiceKey<ChunkedUploads>;
|
|
21
|
+
|
|
13
22
|
export function filesRoutes(storage: ObjectStore, identity: RequestIdentity): ElysiaPlugin {
|
|
14
23
|
return new Elysia({ name: "podokit.files" })
|
|
15
24
|
.post("/files", async ({ request, body, set }) => {
|
|
16
25
|
const key = uploadKey(await identity.userId(request), body.file.name);
|
|
17
|
-
await storage.put(
|
|
18
|
-
key,
|
|
19
|
-
new Uint8Array(await body.file.arrayBuffer()),
|
|
20
|
-
{ contentType: body.file.type },
|
|
21
|
-
);
|
|
26
|
+
await storage.put(key, body.file, { contentType: body.file.type });
|
|
22
27
|
set.status = 201;
|
|
23
28
|
return { key, url: await storage.getDownloadUrl(key) };
|
|
24
29
|
}, {
|
|
@@ -45,10 +50,34 @@ export function filesRoutes(storage: ObjectStore, identity: RequestIdentity): El
|
|
|
45
50
|
});
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
const filesPlugin: AppPlugin = ({ services }) =>
|
|
49
|
-
|
|
53
|
+
const filesPlugin: AppPlugin = ({ env, services }) => {
|
|
54
|
+
const uploads = services.resolve(CHUNKED_UPLOADS);
|
|
55
|
+
const identity = services.resolve(REQUEST_IDENTITY);
|
|
56
|
+
// Registered here rather than in configure: rate-limit may configure later.
|
|
57
|
+
services.tryResolve(RATE_LIMIT_ROUTES)?.register({
|
|
58
|
+
method: "PUT",
|
|
59
|
+
pathPrefix: "/files/uploads",
|
|
60
|
+
profile: { name: "file-uploads", limit: uploads.settings.rateLimitMax },
|
|
61
|
+
});
|
|
62
|
+
return new Elysia({ name: "podokit.files.module" })
|
|
63
|
+
.use(filesRoutes(services.resolve(OBJECT_STORAGE), identity))
|
|
64
|
+
.use(chunkedUploadRoutes(uploads, identity, allowedOrigins(env.CORS_ORIGIN)));
|
|
65
|
+
};
|
|
50
66
|
|
|
51
67
|
export const filesModule: PodokitModule = {
|
|
52
68
|
name: "file-upload",
|
|
69
|
+
configure: (env, services): void => {
|
|
70
|
+
const database = services.resolve(DATABASE);
|
|
71
|
+
const uploads = new ChunkedUploads({
|
|
72
|
+
sql: database.sql,
|
|
73
|
+
dialect: database.provider,
|
|
74
|
+
config: uploadConfig(process.env, apiServerOptions(env).maxRequestBodySize),
|
|
75
|
+
// Resolved on use: an application may replace these after this point.
|
|
76
|
+
storage: () => services.resolve(OBJECT_STORAGE),
|
|
77
|
+
logger: { warn: (details, message) => services.resolve(LOGGER).warn(details, message) },
|
|
78
|
+
});
|
|
79
|
+
services.register(CHUNKED_UPLOADS, uploads, () => uploads.close());
|
|
80
|
+
services.onStart(() => uploads.start());
|
|
81
|
+
},
|
|
53
82
|
plugin: filesPlugin,
|
|
54
83
|
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { MAX_MULTIPART_PARTS, MIN_MULTIPART_PART_BYTES } from "@podosoft/podokit-runtime";
|
|
2
|
+
|
|
3
|
+
const MIB = 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
export interface UploadConfig {
|
|
6
|
+
/** The largest file one chunked upload may hold. */
|
|
7
|
+
maxBytes: number;
|
|
8
|
+
/** Part size for uploads small enough to stay within MAX_MULTIPART_PARTS. */
|
|
9
|
+
partBytes: number;
|
|
10
|
+
/** Seconds an open upload may sit idle; each stored part restarts the clock. */
|
|
11
|
+
sessionTtlSeconds: number;
|
|
12
|
+
/** Seconds between sweeps of expired uploads. 0 turns the sweep off. */
|
|
13
|
+
sweepIntervalSeconds: number;
|
|
14
|
+
/** Part uploads per caller in each rate-limit window, when rate-limit is installed. */
|
|
15
|
+
rateLimitMax: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_UPLOAD_CONFIG: UploadConfig = {
|
|
19
|
+
maxBytes: 1024 * MIB,
|
|
20
|
+
partBytes: 8 * MIB,
|
|
21
|
+
sessionTtlSeconds: 24 * 60 * 60,
|
|
22
|
+
sweepIntervalSeconds: 5 * 60,
|
|
23
|
+
rateLimitMax: 3000,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface PartLayout {
|
|
27
|
+
partSize: number;
|
|
28
|
+
partCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Splits `size` bytes into parts of at least `partBytes`. Files that would need
|
|
33
|
+
* more than MAX_MULTIPART_PARTS parts get larger parts, rounded up to a whole
|
|
34
|
+
* MiB, instead.
|
|
35
|
+
*/
|
|
36
|
+
export function partLayout(size: number, partBytes: number): PartLayout {
|
|
37
|
+
const needed = Math.ceil(size / MAX_MULTIPART_PARTS);
|
|
38
|
+
const partSize = Math.max(partBytes, Math.ceil(needed / MIB) * MIB);
|
|
39
|
+
return { partSize, partCount: Math.max(1, Math.ceil(size / partSize)) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The byte count part `partNumber` must have: `partSize`, except the last part. */
|
|
43
|
+
export function expectedPartSize(size: number, layout: PartLayout, partNumber: number): number {
|
|
44
|
+
return partNumber < layout.partCount ? layout.partSize : size - layout.partSize * (layout.partCount - 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function wholeNumber(
|
|
48
|
+
source: Record<string, string | undefined>,
|
|
49
|
+
name: string,
|
|
50
|
+
fallback: number,
|
|
51
|
+
minimum: number,
|
|
52
|
+
): number {
|
|
53
|
+
const text = source[name]?.trim() ?? "";
|
|
54
|
+
if (text === "") return fallback;
|
|
55
|
+
const value = Number(text);
|
|
56
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
57
|
+
throw new Error(`${name} must be a whole number of at least ${minimum}`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reads the FILE_UPLOAD_* settings. `maxRequestBodySize` is the API server's
|
|
64
|
+
* body limit: the part size of the largest allowed upload must fit in it.
|
|
65
|
+
*/
|
|
66
|
+
export function uploadConfig(
|
|
67
|
+
source: Record<string, string | undefined>,
|
|
68
|
+
maxRequestBodySize: number,
|
|
69
|
+
): UploadConfig {
|
|
70
|
+
const config: UploadConfig = {
|
|
71
|
+
maxBytes: wholeNumber(source, "FILE_UPLOAD_MAX_BYTES", DEFAULT_UPLOAD_CONFIG.maxBytes, 1),
|
|
72
|
+
partBytes: wholeNumber(
|
|
73
|
+
source,
|
|
74
|
+
"FILE_UPLOAD_PART_BYTES",
|
|
75
|
+
DEFAULT_UPLOAD_CONFIG.partBytes,
|
|
76
|
+
MIN_MULTIPART_PART_BYTES,
|
|
77
|
+
),
|
|
78
|
+
sessionTtlSeconds: wholeNumber(
|
|
79
|
+
source,
|
|
80
|
+
"FILE_UPLOAD_SESSION_TTL",
|
|
81
|
+
DEFAULT_UPLOAD_CONFIG.sessionTtlSeconds,
|
|
82
|
+
60,
|
|
83
|
+
),
|
|
84
|
+
sweepIntervalSeconds: wholeNumber(
|
|
85
|
+
source,
|
|
86
|
+
"FILE_UPLOAD_SWEEP_INTERVAL",
|
|
87
|
+
DEFAULT_UPLOAD_CONFIG.sweepIntervalSeconds,
|
|
88
|
+
0,
|
|
89
|
+
),
|
|
90
|
+
rateLimitMax: wholeNumber(source, "FILE_UPLOAD_RATE_LIMIT_MAX", DEFAULT_UPLOAD_CONFIG.rateLimitMax, 1),
|
|
91
|
+
};
|
|
92
|
+
const largestPart = partLayout(config.maxBytes, config.partBytes).partSize;
|
|
93
|
+
if (largestPart > maxRequestBodySize) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Chunked uploads of FILE_UPLOAD_MAX_BYTES need ${largestPart}-byte parts, more than the API body limit of ${maxRequestBodySize} bytes; raise API_MAX_REQUEST_BODY_SIZE or lower FILE_UPLOAD_PART_BYTES or FILE_UPLOAD_MAX_BYTES`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return config;
|
|
99
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class InitFileUploadSessions1792100000000 implements MigrationInterface {
|
|
4
|
+
name = "InitFileUploadSessions1792100000000";
|
|
5
|
+
|
|
6
|
+
async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
const sqlite = queryRunner.connection.options.type === "better-sqlite3";
|
|
8
|
+
const id = sqlite ? "text" : "uuid";
|
|
9
|
+
const timestamp = sqlite ? "text" : "TIMESTAMP WITH TIME ZONE";
|
|
10
|
+
await queryRunner.query(`
|
|
11
|
+
CREATE TABLE "file_upload_sessions" (
|
|
12
|
+
"id" ${id} NOT NULL,
|
|
13
|
+
"ownerId" character varying(128),
|
|
14
|
+
"objectKey" character varying(1024) NOT NULL,
|
|
15
|
+
"multipartUploadId" character varying(1024),
|
|
16
|
+
"filename" character varying(255) NOT NULL,
|
|
17
|
+
"contentType" character varying(255),
|
|
18
|
+
"size" bigint NOT NULL,
|
|
19
|
+
"partSize" bigint NOT NULL,
|
|
20
|
+
"partCount" integer NOT NULL,
|
|
21
|
+
"status" character varying(16) NOT NULL,
|
|
22
|
+
"expiresAt" ${timestamp} NOT NULL,
|
|
23
|
+
"createdAt" ${timestamp} NOT NULL,
|
|
24
|
+
"updatedAt" ${timestamp} NOT NULL,
|
|
25
|
+
CONSTRAINT "PK_file_upload_sessions_id" PRIMARY KEY ("id")
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
await queryRunner.query(
|
|
29
|
+
`CREATE INDEX "IDX_file_upload_sessions_status_expiresAt" ON "file_upload_sessions" ("status", "expiresAt")`,
|
|
30
|
+
);
|
|
31
|
+
await queryRunner.query(`
|
|
32
|
+
CREATE TABLE "file_upload_parts" (
|
|
33
|
+
"sessionId" ${id} NOT NULL,
|
|
34
|
+
"partNumber" integer NOT NULL,
|
|
35
|
+
"etag" character varying(1024) NOT NULL,
|
|
36
|
+
"size" bigint NOT NULL,
|
|
37
|
+
"updatedAt" ${timestamp} NOT NULL,
|
|
38
|
+
CONSTRAINT "PK_file_upload_parts" PRIMARY KEY ("sessionId", "partNumber"),
|
|
39
|
+
CONSTRAINT "FK_file_upload_parts_sessionId" FOREIGN KEY ("sessionId")
|
|
40
|
+
REFERENCES "file_upload_sessions" ("id") ON DELETE CASCADE
|
|
41
|
+
)
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async down(queryRunner: QueryRunner): Promise<void> {
|
|
46
|
+
await queryRunner.query(`DROP TABLE "file_upload_parts"`);
|
|
47
|
+
await queryRunner.query(`DROP TABLE "file_upload_sessions"`);
|
|
48
|
+
}
|
|
49
|
+
}
|