@crowi/plugin-storage-aws-s3 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Sotaro KARASAWA <sotaro.k@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # @crowi/plugin-storage-aws-s3
2
+
3
+ AWS S3 storage driver for Crowi 2.0. Stores page attachments and profile
4
+ pictures in an S3 bucket. Pairs with [`@crowi/plugin-aws`](../plugin-aws)
5
+ for shared region / access-key configuration.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ crowi-admin plugin add @crowi/plugin-storage-aws-s3
11
+ ```
12
+
13
+ (or, in dev: `pnpm --filter @crowi/api add -D @crowi/plugin-storage-aws-s3`)
14
+
15
+ The plugin auto-loads its dependency `@crowi/plugin-aws`. You don't need
16
+ to add it explicitly.
17
+
18
+ ## Configure
19
+
20
+ ### 1. Activate the driver in `crowi.config.json`
21
+
22
+ ```jsonc
23
+ {
24
+ "plugins": ["@crowi/plugin-storage-aws-s3"],
25
+ "storage": { "driver": "s3" }
26
+ }
27
+ ```
28
+
29
+ A server restart is required when `storage.driver` changes — Crowi reads
30
+ this file once at boot.
31
+
32
+ ### 2. Fill in credentials in the admin UI
33
+
34
+ Open `/admin/plugins`:
35
+
36
+ - **`@crowi/plugin-aws`** — `region`, `accessKeyId`, `secretAccessKey`
37
+ (the secret is encrypted at rest with `CROWI_ENCRYPTION_KEY`)
38
+ - **`@crowi/plugin-storage-aws-s3`** — `bucket`
39
+
40
+ Leaving `accessKeyId` and `secretAccessKey` blank tells the SDK to use
41
+ its [default credential chain](https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html#credentialProviderChain)
42
+ (env vars, shared credentials file, EC2 instance role, ECS task role,
43
+ EKS pod identity, etc). This is the recommended setup when running on
44
+ AWS — let the platform vend short-lived credentials, don't store
45
+ long-lived keys in Mongo.
46
+
47
+ ### 3. Migrate existing files (if switching from `local`)
48
+
49
+ ```bash
50
+ crowi-admin storage copy --from local --to s3 --dry-run # preview
51
+ crowi-admin storage copy --from local --to s3 # actual copy
52
+ ```
53
+
54
+ The CLI walks the `Attachment` collection and `User.image` URLs, copying
55
+ every key from one driver to the other. Failures are logged and skipped;
56
+ re-running is safe (S3 `PutObject` is overwrite-by-key).
57
+
58
+ ## Required IAM permissions
59
+
60
+ Three S3 operations are called by the driver: `PutObject`, `GetObject`,
61
+ `DeleteObject`. Signed URLs are produced locally with the SDK presigner,
62
+ which signs a `GetObjectCommand` — so the **client** consuming the signed
63
+ URL needs `GetObject` (which the driver's IAM principal already has).
64
+ `ListBucket` is **not** needed.
65
+
66
+ A minimal IAM policy you can attach to the IAM user / role Crowi runs as:
67
+
68
+ ```json
69
+ {
70
+ "Version": "2012-10-17",
71
+ "Statement": [
72
+ {
73
+ "Sid": "CrowiStorageObjects",
74
+ "Effect": "Allow",
75
+ "Action": [
76
+ "s3:PutObject",
77
+ "s3:GetObject",
78
+ "s3:DeleteObject"
79
+ ],
80
+ "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
81
+ }
82
+ ]
83
+ }
84
+ ```
85
+
86
+ The same policy is shipped at [`examples/iam-policy.json`](examples/iam-policy.json)
87
+ so you can apply it directly:
88
+
89
+ ```bash
90
+ aws iam create-policy \
91
+ --policy-name CrowiStorage \
92
+ --policy-document file://examples/iam-policy.json
93
+ ```
94
+
95
+ A variant with `s3:ListBucket` enabled is at
96
+ [`examples/iam-policy-with-list.json`](examples/iam-policy-with-list.json) —
97
+ not needed today, but reserved for a future `crowi-admin storage list` /
98
+ `storage diff` command that would enumerate keys server-side.
99
+
100
+ ## What does NOT need to be configured
101
+
102
+ - **Bucket policy** — IAM policy is enough for normal single-account
103
+ setups. Use a bucket policy only if you need cross-account access,
104
+ SSE enforcement, or public-read overrides — Crowi neither requires
105
+ nor opposes those, but they're environment-specific and out of scope
106
+ for this plugin's docs.
107
+ - **CORS** — Crowi serves signed URLs directly via `<img>` / download
108
+ links, which are simple `GET` requests with no preflight. CORS rules
109
+ on the bucket are unnecessary unless you add a future feature that
110
+ uploads from the browser straight to S3 (presigned `PUT`).
111
+ - **SSE-S3** — works out of the box if the bucket has default
112
+ encryption enabled. The driver does not set `ServerSideEncryption`
113
+ explicitly; let the bucket's default policy decide.
114
+
115
+ ## Object layout
116
+
117
+ The driver passes storage keys through to S3 verbatim. The keys Crowi
118
+ uses are:
119
+
120
+ ```
121
+ attachment/<pageId>/<fileId>/<original-filename>
122
+ user/<userId>.<ext>
123
+ ```
124
+
125
+ This matches the v1.x layout, so operators upgrading from Crowi 1.x can
126
+ point `bucket` at their existing bucket and files round-trip without
127
+ migration.
128
+
129
+ ## Troubleshooting
130
+
131
+ | Symptom | Likely cause |
132
+ |---|---|
133
+ | `bucket=<unset>` in the boot log | Set `bucket` under `/admin/plugins` for `@crowi/plugin-storage-aws-s3`. |
134
+ | `region=<default>` in the boot log | Set `region` under `/admin/plugins` for `@crowi/plugin-aws` (e.g. `ap-northeast-1`). |
135
+ | `403 Forbidden` on PUT | Check the IAM policy — `s3:PutObject` against `arn:aws:s3:::<bucket>/*`. |
136
+ | `403 Forbidden` on GET via signed URL | Same; the signing principal needs `s3:GetObject`. |
137
+ | `301 PermanentRedirect` | `region` mismatch. The bucket lives in a different region than the SDK is configured to talk to. |
138
+ | Long-lived secret keys committed somewhere | Don't. Use IAM roles on EC2 / ECS / EKS, leave `accessKeyId` blank. |
139
+
140
+ ## See also
141
+
142
+ - [`@crowi/plugin-aws`](../plugin-aws) — shared AWS credential plugin (auto-loaded as a dependency).
143
+ - [`@crowi/plugin-storage-local`](../plugin-storage-local) — the default-on local filesystem driver.
144
+ - RFC-0001 §"Storage (S3)" for the migration story from Crowi 1.x.
@@ -0,0 +1,18 @@
1
+ import { S3Client } from '@aws-sdk/client-s3';
2
+ import { StorageDriver, CrowiPlugin } from '@crowi/plugin-api';
3
+
4
+ interface S3DriverState {
5
+ client: S3Client;
6
+ bucket: string;
7
+ }
8
+ declare const plugin: CrowiPlugin;
9
+
10
+ /**
11
+ * Build the storage driver. Methods read `driverState` *once at the
12
+ * top* — a snapshot — so a `reconfigure` running concurrently with an
13
+ * inflight `put` / `get` cannot swap the client mid-call. The next
14
+ * call sees the new client/bucket.
15
+ */
16
+ declare function createS3Driver(driverState: S3DriverState): StorageDriver;
17
+
18
+ export { type S3DriverState, createS3Driver, plugin as default };
@@ -0,0 +1,18 @@
1
+ import { S3Client } from '@aws-sdk/client-s3';
2
+ import { StorageDriver, CrowiPlugin } from '@crowi/plugin-api';
3
+
4
+ interface S3DriverState {
5
+ client: S3Client;
6
+ bucket: string;
7
+ }
8
+ declare const plugin: CrowiPlugin;
9
+
10
+ /**
11
+ * Build the storage driver. Methods read `driverState` *once at the
12
+ * top* — a snapshot — so a `reconfigure` running concurrently with an
13
+ * inflight `put` / `get` cannot swap the client mid-call. The next
14
+ * call sees the new client/bucket.
15
+ */
16
+ declare function createS3Driver(driverState: S3DriverState): StorageDriver;
17
+
18
+ export { type S3DriverState, createS3Driver, plugin as default };
package/dist/index.js ADDED
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createS3Driver: () => createS3Driver,
24
+ default: () => index_default
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ var import_v3 = require("zod/v3");
28
+ var import_client_s3 = require("@aws-sdk/client-s3");
29
+ var import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
30
+ var S3StorageConfigSchema = import_v3.z.object({
31
+ bucket: import_v3.z.string().default("")
32
+ }).strict();
33
+ var state = {
34
+ client: new import_client_s3.S3Client({}),
35
+ bucket: ""
36
+ };
37
+ var plugin = {
38
+ name: "@crowi/plugin-storage-aws-s3",
39
+ version: "0.1.0-dev",
40
+ requires: ["@crowi/plugin-aws"],
41
+ configSchema: S3StorageConfigSchema,
42
+ adminPlacement: {
43
+ label: "AWS S3",
44
+ icon: "cloud"
45
+ // section omitted: derived from registerStorage → 'storage'
46
+ },
47
+ registerStorage: (registry, ctx) => {
48
+ applyConfigToState(ctx, state);
49
+ registry.register("s3", createS3Driver(state));
50
+ ctx.log.debug("registered s3 storage driver (bucket=%s)", state.bucket || "<unset>");
51
+ },
52
+ reconfigure: (ctx) => {
53
+ applyConfigToState(ctx, state);
54
+ ctx.log.debug("reconfigured s3 storage driver (bucket=%s)", state.bucket || "<unset>");
55
+ }
56
+ };
57
+ var index_default = plugin;
58
+ function applyConfigToState(ctx, target) {
59
+ const own = ctx.config();
60
+ const aws = ctx.dependencyConfig("@crowi/plugin-aws");
61
+ target.client = new import_client_s3.S3Client({
62
+ region: aws.region || void 0,
63
+ credentials: aws.accessKeyId && aws.secretAccessKey ? {
64
+ accessKeyId: aws.accessKeyId,
65
+ secretAccessKey: aws.secretAccessKey
66
+ } : void 0
67
+ });
68
+ target.bucket = own.bucket;
69
+ }
70
+ function createS3Driver(driverState) {
71
+ return {
72
+ async put(key, body, meta) {
73
+ const { client, bucket } = driverState;
74
+ requireBucket(bucket);
75
+ await client.send(
76
+ new import_client_s3.PutObjectCommand({
77
+ Bucket: bucket,
78
+ Key: key,
79
+ Body: body,
80
+ ContentType: meta.contentType
81
+ })
82
+ );
83
+ return { key };
84
+ },
85
+ async get(key) {
86
+ const { client, bucket } = driverState;
87
+ requireBucket(bucket);
88
+ const response = await client.send(new import_client_s3.GetObjectCommand({ Bucket: bucket, Key: key }));
89
+ const body = response.Body;
90
+ if (!body) {
91
+ throw new Error(`S3 returned empty body for key '${key}'`);
92
+ }
93
+ return body;
94
+ },
95
+ async delete(key) {
96
+ const { client, bucket } = driverState;
97
+ requireBucket(bucket);
98
+ await client.send(new import_client_s3.DeleteObjectCommand({ Bucket: bucket, Key: key }));
99
+ },
100
+ async signedUrl(key, expiresInSec) {
101
+ const { client, bucket } = driverState;
102
+ requireBucket(bucket);
103
+ return (0, import_s3_request_presigner.getSignedUrl)(client, new import_client_s3.GetObjectCommand({ Bucket: bucket, Key: key }), { expiresIn: expiresInSec });
104
+ }
105
+ };
106
+ }
107
+ function requireBucket(bucket) {
108
+ if (!bucket) {
109
+ throw new Error("@crowi/plugin-storage-aws-s3: bucket is not configured.");
110
+ }
111
+ }
112
+ // Annotate the CommonJS export names for ESM import in node:
113
+ 0 && (module.exports = {
114
+ createS3Driver
115
+ });
116
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { Readable } from 'node:stream';\nimport { z } from 'zod/v3';\nimport { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport type { AwsConfig } from '@crowi/plugin-aws';\nimport type { CrowiPlugin, PluginContext, StorageDriver } from '@crowi/plugin-api';\n\nconst S3StorageConfigSchema = z\n .object({\n bucket: z.string().default(''),\n })\n .strict();\n\ntype S3StorageConfig = z.infer<typeof S3StorageConfigSchema>;\n\nexport interface S3DriverState {\n client: S3Client;\n bucket: string;\n}\n\n/**\n * Module-scope state ref. `registerStorage` initialises it from the\n * boot-time config; the driver methods snapshot from it on every call;\n * `reconfigure` mutates its fields in place when admin saves new\n * values. The single-instance assumption is fine — the plugin\n * registers exactly one `'s3'` driver, owned by this module.\n */\nconst state: S3DriverState = {\n client: new S3Client({}),\n bucket: '',\n};\n\nconst plugin: CrowiPlugin = {\n name: '@crowi/plugin-storage-aws-s3',\n version: '0.1.0-dev',\n requires: ['@crowi/plugin-aws'],\n configSchema: S3StorageConfigSchema,\n adminPlacement: {\n label: 'AWS S3',\n icon: 'cloud',\n // section omitted: derived from registerStorage → 'storage'\n },\n\n registerStorage: (registry, ctx) => {\n applyConfigToState(ctx, state);\n registry.register('s3', createS3Driver(state));\n ctx.log.debug('registered s3 storage driver (bucket=%s)', state.bucket || '<unset>');\n },\n\n reconfigure: (ctx) => {\n applyConfigToState(ctx, state);\n ctx.log.debug('reconfigured s3 storage driver (bucket=%s)', state.bucket || '<unset>');\n },\n};\n\nexport default plugin;\n\nfunction applyConfigToState(ctx: PluginContext, target: S3DriverState): void {\n const own = ctx.config<S3StorageConfig>();\n const aws = ctx.dependencyConfig<AwsConfig>('@crowi/plugin-aws');\n target.client = new S3Client({\n region: aws.region || undefined,\n credentials:\n aws.accessKeyId && aws.secretAccessKey\n ? {\n accessKeyId: aws.accessKeyId,\n secretAccessKey: aws.secretAccessKey,\n }\n : undefined,\n });\n target.bucket = own.bucket;\n}\n\n/**\n * Build the storage driver. Methods read `driverState` *once at the\n * top* — a snapshot — so a `reconfigure` running concurrently with an\n * inflight `put` / `get` cannot swap the client mid-call. The next\n * call sees the new client/bucket.\n */\nexport function createS3Driver(driverState: S3DriverState): StorageDriver {\n return {\n async put(key, body, meta) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n await client.send(\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: body as Buffer | Readable,\n ContentType: meta.contentType,\n }),\n );\n return { key };\n },\n\n async get(key) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));\n const body = response.Body;\n if (!body) {\n throw new Error(`S3 returned empty body for key '${key}'`);\n }\n return body as Readable;\n },\n\n async delete(key) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));\n },\n\n async signedUrl(key, expiresInSec) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n return getSignedUrl(client, new GetObjectCommand({ Bucket: bucket, Key: key }), { expiresIn: expiresInSec });\n },\n };\n}\n\nfunction requireBucket(bucket: string): void {\n if (!bucket) {\n throw new Error('@crowi/plugin-storage-aws-s3: bucket is not configured.');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,gBAAkB;AAClB,uBAAkF;AAClF,kCAA6B;AAI7B,IAAM,wBAAwB,YAC3B,OAAO;AAAA,EACN,QAAQ,YAAE,OAAO,EAAE,QAAQ,EAAE;AAC/B,CAAC,EACA,OAAO;AAgBV,IAAM,QAAuB;AAAA,EAC3B,QAAQ,IAAI,0BAAS,CAAC,CAAC;AAAA,EACvB,QAAQ;AACV;AAEA,IAAM,SAAsB;AAAA,EAC1B,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU,CAAC,mBAAmB;AAAA,EAC9B,cAAc;AAAA,EACd,gBAAgB;AAAA,IACd,OAAO;AAAA,IACP,MAAM;AAAA;AAAA,EAER;AAAA,EAEA,iBAAiB,CAAC,UAAU,QAAQ;AAClC,uBAAmB,KAAK,KAAK;AAC7B,aAAS,SAAS,MAAM,eAAe,KAAK,CAAC;AAC7C,QAAI,IAAI,MAAM,4CAA4C,MAAM,UAAU,SAAS;AAAA,EACrF;AAAA,EAEA,aAAa,CAAC,QAAQ;AACpB,uBAAmB,KAAK,KAAK;AAC7B,QAAI,IAAI,MAAM,8CAA8C,MAAM,UAAU,SAAS;AAAA,EACvF;AACF;AAEA,IAAO,gBAAQ;AAEf,SAAS,mBAAmB,KAAoB,QAA6B;AAC3E,QAAM,MAAM,IAAI,OAAwB;AACxC,QAAM,MAAM,IAAI,iBAA4B,mBAAmB;AAC/D,SAAO,SAAS,IAAI,0BAAS;AAAA,IAC3B,QAAQ,IAAI,UAAU;AAAA,IACtB,aACE,IAAI,eAAe,IAAI,kBACnB;AAAA,MACE,aAAa,IAAI;AAAA,MACjB,iBAAiB,IAAI;AAAA,IACvB,IACA;AAAA,EACR,CAAC;AACD,SAAO,SAAS,IAAI;AACtB;AAQO,SAAS,eAAe,aAA2C;AACxE,SAAO;AAAA,IACL,MAAM,IAAI,KAAK,MAAM,MAAM;AACzB,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,YAAM,OAAO;AAAA,QACX,IAAI,kCAAiB;AAAA,UACnB,QAAQ;AAAA,UACR,KAAK;AAAA,UACL,MAAM;AAAA,UACN,aAAa,KAAK;AAAA,QACpB,CAAC;AAAA,MACH;AACA,aAAO,EAAE,IAAI;AAAA,IACf;AAAA,IAEA,MAAM,IAAI,KAAK;AACb,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,YAAM,WAAW,MAAM,OAAO,KAAK,IAAI,kCAAiB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,CAAC;AACrF,YAAM,OAAO,SAAS;AACtB,UAAI,CAAC,MAAM;AACT,cAAM,IAAI,MAAM,mCAAmC,GAAG,GAAG;AAAA,MAC3D;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,OAAO,KAAK;AAChB,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,YAAM,OAAO,KAAK,IAAI,qCAAoB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,CAAC;AAAA,IACzE;AAAA,IAEA,MAAM,UAAU,KAAK,cAAc;AACjC,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,iBAAO,0CAAa,QAAQ,IAAI,kCAAiB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,GAAG,EAAE,WAAW,aAAa,CAAC;AAAA,IAC7G;AAAA,EACF;AACF;AAEA,SAAS,cAAc,QAAsB;AAC3C,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AACF;","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,91 @@
1
+ // src/index.ts
2
+ import { z } from "zod/v3";
3
+ import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
4
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
5
+ var S3StorageConfigSchema = z.object({
6
+ bucket: z.string().default("")
7
+ }).strict();
8
+ var state = {
9
+ client: new S3Client({}),
10
+ bucket: ""
11
+ };
12
+ var plugin = {
13
+ name: "@crowi/plugin-storage-aws-s3",
14
+ version: "0.1.0-dev",
15
+ requires: ["@crowi/plugin-aws"],
16
+ configSchema: S3StorageConfigSchema,
17
+ adminPlacement: {
18
+ label: "AWS S3",
19
+ icon: "cloud"
20
+ // section omitted: derived from registerStorage → 'storage'
21
+ },
22
+ registerStorage: (registry, ctx) => {
23
+ applyConfigToState(ctx, state);
24
+ registry.register("s3", createS3Driver(state));
25
+ ctx.log.debug("registered s3 storage driver (bucket=%s)", state.bucket || "<unset>");
26
+ },
27
+ reconfigure: (ctx) => {
28
+ applyConfigToState(ctx, state);
29
+ ctx.log.debug("reconfigured s3 storage driver (bucket=%s)", state.bucket || "<unset>");
30
+ }
31
+ };
32
+ var index_default = plugin;
33
+ function applyConfigToState(ctx, target) {
34
+ const own = ctx.config();
35
+ const aws = ctx.dependencyConfig("@crowi/plugin-aws");
36
+ target.client = new S3Client({
37
+ region: aws.region || void 0,
38
+ credentials: aws.accessKeyId && aws.secretAccessKey ? {
39
+ accessKeyId: aws.accessKeyId,
40
+ secretAccessKey: aws.secretAccessKey
41
+ } : void 0
42
+ });
43
+ target.bucket = own.bucket;
44
+ }
45
+ function createS3Driver(driverState) {
46
+ return {
47
+ async put(key, body, meta) {
48
+ const { client, bucket } = driverState;
49
+ requireBucket(bucket);
50
+ await client.send(
51
+ new PutObjectCommand({
52
+ Bucket: bucket,
53
+ Key: key,
54
+ Body: body,
55
+ ContentType: meta.contentType
56
+ })
57
+ );
58
+ return { key };
59
+ },
60
+ async get(key) {
61
+ const { client, bucket } = driverState;
62
+ requireBucket(bucket);
63
+ const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
64
+ const body = response.Body;
65
+ if (!body) {
66
+ throw new Error(`S3 returned empty body for key '${key}'`);
67
+ }
68
+ return body;
69
+ },
70
+ async delete(key) {
71
+ const { client, bucket } = driverState;
72
+ requireBucket(bucket);
73
+ await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
74
+ },
75
+ async signedUrl(key, expiresInSec) {
76
+ const { client, bucket } = driverState;
77
+ requireBucket(bucket);
78
+ return getSignedUrl(client, new GetObjectCommand({ Bucket: bucket, Key: key }), { expiresIn: expiresInSec });
79
+ }
80
+ };
81
+ }
82
+ function requireBucket(bucket) {
83
+ if (!bucket) {
84
+ throw new Error("@crowi/plugin-storage-aws-s3: bucket is not configured.");
85
+ }
86
+ }
87
+ export {
88
+ createS3Driver,
89
+ index_default as default
90
+ };
91
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { Readable } from 'node:stream';\nimport { z } from 'zod/v3';\nimport { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport type { AwsConfig } from '@crowi/plugin-aws';\nimport type { CrowiPlugin, PluginContext, StorageDriver } from '@crowi/plugin-api';\n\nconst S3StorageConfigSchema = z\n .object({\n bucket: z.string().default(''),\n })\n .strict();\n\ntype S3StorageConfig = z.infer<typeof S3StorageConfigSchema>;\n\nexport interface S3DriverState {\n client: S3Client;\n bucket: string;\n}\n\n/**\n * Module-scope state ref. `registerStorage` initialises it from the\n * boot-time config; the driver methods snapshot from it on every call;\n * `reconfigure` mutates its fields in place when admin saves new\n * values. The single-instance assumption is fine — the plugin\n * registers exactly one `'s3'` driver, owned by this module.\n */\nconst state: S3DriverState = {\n client: new S3Client({}),\n bucket: '',\n};\n\nconst plugin: CrowiPlugin = {\n name: '@crowi/plugin-storage-aws-s3',\n version: '0.1.0-dev',\n requires: ['@crowi/plugin-aws'],\n configSchema: S3StorageConfigSchema,\n adminPlacement: {\n label: 'AWS S3',\n icon: 'cloud',\n // section omitted: derived from registerStorage → 'storage'\n },\n\n registerStorage: (registry, ctx) => {\n applyConfigToState(ctx, state);\n registry.register('s3', createS3Driver(state));\n ctx.log.debug('registered s3 storage driver (bucket=%s)', state.bucket || '<unset>');\n },\n\n reconfigure: (ctx) => {\n applyConfigToState(ctx, state);\n ctx.log.debug('reconfigured s3 storage driver (bucket=%s)', state.bucket || '<unset>');\n },\n};\n\nexport default plugin;\n\nfunction applyConfigToState(ctx: PluginContext, target: S3DriverState): void {\n const own = ctx.config<S3StorageConfig>();\n const aws = ctx.dependencyConfig<AwsConfig>('@crowi/plugin-aws');\n target.client = new S3Client({\n region: aws.region || undefined,\n credentials:\n aws.accessKeyId && aws.secretAccessKey\n ? {\n accessKeyId: aws.accessKeyId,\n secretAccessKey: aws.secretAccessKey,\n }\n : undefined,\n });\n target.bucket = own.bucket;\n}\n\n/**\n * Build the storage driver. Methods read `driverState` *once at the\n * top* — a snapshot — so a `reconfigure` running concurrently with an\n * inflight `put` / `get` cannot swap the client mid-call. The next\n * call sees the new client/bucket.\n */\nexport function createS3Driver(driverState: S3DriverState): StorageDriver {\n return {\n async put(key, body, meta) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n await client.send(\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: body as Buffer | Readable,\n ContentType: meta.contentType,\n }),\n );\n return { key };\n },\n\n async get(key) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));\n const body = response.Body;\n if (!body) {\n throw new Error(`S3 returned empty body for key '${key}'`);\n }\n return body as Readable;\n },\n\n async delete(key) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));\n },\n\n async signedUrl(key, expiresInSec) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n return getSignedUrl(client, new GetObjectCommand({ Bucket: bucket, Key: key }), { expiresIn: expiresInSec });\n },\n };\n}\n\nfunction requireBucket(bucket: string): void {\n if (!bucket) {\n throw new Error('@crowi/plugin-storage-aws-s3: bucket is not configured.');\n }\n}\n"],"mappings":";AACA,SAAS,SAAS;AAClB,SAAS,qBAAqB,kBAAkB,kBAAkB,gBAAgB;AAClF,SAAS,oBAAoB;AAI7B,IAAM,wBAAwB,EAC3B,OAAO;AAAA,EACN,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE;AAC/B,CAAC,EACA,OAAO;AAgBV,IAAM,QAAuB;AAAA,EAC3B,QAAQ,IAAI,SAAS,CAAC,CAAC;AAAA,EACvB,QAAQ;AACV;AAEA,IAAM,SAAsB;AAAA,EAC1B,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU,CAAC,mBAAmB;AAAA,EAC9B,cAAc;AAAA,EACd,gBAAgB;AAAA,IACd,OAAO;AAAA,IACP,MAAM;AAAA;AAAA,EAER;AAAA,EAEA,iBAAiB,CAAC,UAAU,QAAQ;AAClC,uBAAmB,KAAK,KAAK;AAC7B,aAAS,SAAS,MAAM,eAAe,KAAK,CAAC;AAC7C,QAAI,IAAI,MAAM,4CAA4C,MAAM,UAAU,SAAS;AAAA,EACrF;AAAA,EAEA,aAAa,CAAC,QAAQ;AACpB,uBAAmB,KAAK,KAAK;AAC7B,QAAI,IAAI,MAAM,8CAA8C,MAAM,UAAU,SAAS;AAAA,EACvF;AACF;AAEA,IAAO,gBAAQ;AAEf,SAAS,mBAAmB,KAAoB,QAA6B;AAC3E,QAAM,MAAM,IAAI,OAAwB;AACxC,QAAM,MAAM,IAAI,iBAA4B,mBAAmB;AAC/D,SAAO,SAAS,IAAI,SAAS;AAAA,IAC3B,QAAQ,IAAI,UAAU;AAAA,IACtB,aACE,IAAI,eAAe,IAAI,kBACnB;AAAA,MACE,aAAa,IAAI;AAAA,MACjB,iBAAiB,IAAI;AAAA,IACvB,IACA;AAAA,EACR,CAAC;AACD,SAAO,SAAS,IAAI;AACtB;AAQO,SAAS,eAAe,aAA2C;AACxE,SAAO;AAAA,IACL,MAAM,IAAI,KAAK,MAAM,MAAM;AACzB,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,YAAM,OAAO;AAAA,QACX,IAAI,iBAAiB;AAAA,UACnB,QAAQ;AAAA,UACR,KAAK;AAAA,UACL,MAAM;AAAA,UACN,aAAa,KAAK;AAAA,QACpB,CAAC;AAAA,MACH;AACA,aAAO,EAAE,IAAI;AAAA,IACf;AAAA,IAEA,MAAM,IAAI,KAAK;AACb,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,YAAM,WAAW,MAAM,OAAO,KAAK,IAAI,iBAAiB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,CAAC;AACrF,YAAM,OAAO,SAAS;AACtB,UAAI,CAAC,MAAM;AACT,cAAM,IAAI,MAAM,mCAAmC,GAAG,GAAG;AAAA,MAC3D;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,OAAO,KAAK;AAChB,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,YAAM,OAAO,KAAK,IAAI,oBAAoB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,CAAC;AAAA,IACzE;AAAA,IAEA,MAAM,UAAU,KAAK,cAAc;AACjC,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,aAAO,aAAa,QAAQ,IAAI,iBAAiB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,GAAG,EAAE,WAAW,aAAa,CAAC;AAAA,IAC7G;AAAA,EACF;AACF;AAEA,SAAS,cAAc,QAAsB;AAC3C,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@crowi/plugin-storage-aws-s3",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "AWS S3 storage driver for Crowi 2.0. Depends on @crowi/plugin-aws for shared credentials.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "peerDependencies": {
23
+ "zod": "^4.4.3"
24
+ },
25
+ "dependencies": {
26
+ "@aws-sdk/client-s3": "^3.1060.0",
27
+ "@aws-sdk/s3-request-presigner": "^3.1060.0",
28
+ "@crowi/plugin-api": "^0.1.0-alpha.0",
29
+ "@crowi/plugin-aws": "^0.1.0-alpha.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^24",
33
+ "tsup": "^8.3.5",
34
+ "typescript": "^5.8.3",
35
+ "zod": "^4.4.3",
36
+ "@crowi/plugin-aws": "0.1.0-alpha.0",
37
+ "@crowi/tsconfig": "0.1.0-alpha.0",
38
+ "@crowi/plugin-api": "0.1.0-alpha.0"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup",
42
+ "dev": "tsup --watch --no-clean",
43
+ "type-check": "tsc --noEmit"
44
+ }
45
+ }